Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=test
POSTGRES_HOST=backend-db
PGPORT=5433
POSTGRES_PORT=5432

# Celery / Redis
CELERY_BROKER_URL=redis://backend-redis:6379/0
182 changes: 181 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,184 @@ wheels/
.venv
.idea
**/.DS_Store
backend/storage/*
backend/storage/*

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
.run/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# Database
*.sqlite

# User-specific
.DS_Store

# Docker
.dockerignore
.docker
Dockerfile_local

# Docker Compose
docker-compose.override.yml

# VS Code directories
.vscode/

# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp
*.mst

# Windows shortcuts
*.lnk

# JetBrains Pycharm
.idea/

# Python Debugger
.pydevproject

# Custom
*.swp
*.swo
env.sh
id_ed25519

23 changes: 23 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Автор

**Юнгблюд Артур**

## Архитектурные паттерны и подходы

### 🏗️ Архитектура и разделение слоев
* **API-слой:** Реализован через паттерн **Router Factory**. Эндпоинты выполняют роль **тонких контроллеров**, отвечая исключительно за маршрутизацию, конфигурацию OpenAPI и HTTP-контракты.
* **Бизнес-логика:** Выделена в независимые сценарии использования через паттерн **Command**.
* **Слой данных:** Взаимодействие с базами данных и внешними сервисам полностью изолировано с помощью паттерна **Repository**.
* **Связанность компонентов:** Благодаря строгому соблюдению принципа **инверсии зависимостей** удалось органично изолировать части приложения друг от друга. Слои получились независимыми, расширяемыми и легко тестируемыми.

### ⚙️ Управление ресурсами и Concurrency
* **Жизненный цикл:** Все основные переиспользуемые сущности и клиенты подключений инициализируются в рамках **Lifespan**/**CrateApp** и хранятся непосредственно в состоянии приложения (**`app.state`** в FastAPI). Это обеспечивает единую точку конфигурации и эффективное управление системными ресурсами.
* **Асинхронность:** Постарался весь блокирующий код убрать под asyncio.to_thread(celery) и anyio.run_sync(FastApi)

### 🧪 Стратегия тестирования
Высокая изолированность архитектурных слоев позволила выстроить эффективную и быструю модель тестирования:
> 💡 *Примечание:* Данный проект выполнен примерно в том же стиле что и мой же проект https://github.com/BlopFlop/search_docs/tree/dev/tests поэтому если бы я писал тесты то это выглядело бы так
* **Smoke-тесты:** Выступают в роли первой линии проверки. Они валидируют успешный запуск базы данных и проверяют доступность приложения через эндпоинт Healthcheck.
* **Функциональные тесты:** Проверяют целиком изолированный объект резолвера, подменяя внешний мир и базы данных быстрыми заглушками репозиториев.
* **Интеграционные тесты:** Покрывают исключительно слой репозиториев **Repository**, проверяя корректность запросов в реальной, изолированной тестовой среде (БД и Elasticsearch).
* **E2E-тесты:** Запускают приложение целиком в тестовом окружении и проверяют по одному ключевому положительному сценарию на каждый сквозной процесс.
16 changes: 16 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import uvicorn

from alembic import command, config


if __name__ == "__main__":
from src.app import app

alembic_cfg = config.Config("alembic.ini")
command.upgrade(alembic_cfg, "head")

uvicorn.run(
app,
host=app.state.settings.SERVICE_HOST,
port=app.state.settings.SERVICE_PORT,
)
6 changes: 3 additions & 3 deletions backend/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from src.service import DB_URL
from src.config import init_settings
from src.models import Base
import src.models

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
settings = init_settings()
config = context.config
config.set_main_option('sqlalchemy.url', DB_URL)
config.set_main_option('sqlalchemy.url', settings.database.build_dsn())

# Interpret the config file for Python logging.
# This line sets up loggers basically.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""init

Revision ID: 0d6439d2e79f
Revision ID: 237cee72ccc1
Revises:
Create Date: 2026-04-03 16:26:49.885174
Create Date: 2026-07-11 13:03:10.860548

"""
from typing import Sequence, Union
Expand All @@ -12,7 +12,7 @@


# revision identifiers, used by Alembic.
revision: str = '0d6439d2e79f'
revision: str = '237cee72ccc1'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
Expand All @@ -22,37 +22,55 @@ def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('files',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=False),
sa.Column('original_name', sa.String(length=255), nullable=False),
sa.Column('stored_name', sa.String(length=255), nullable=False),
sa.Column('mime_type', sa.String(length=255), nullable=False),
sa.Column('size', sa.Integer(), nullable=False),
sa.Column('processing_status', sa.String(length=50), nullable=False),
sa.Column('scan_status', sa.String(length=50), nullable=True),
sa.Column('processing_status', sa.Enum('uploaded', 'processing', 'failed', 'processed', name='processingstatus'), nullable=False),
sa.Column('scan_status', sa.Enum('suspicious', 'clean', 'failed', name='scanstatus'), nullable=True),
sa.Column('scan_details', sa.String(length=500), nullable=True),
sa.Column('metadata_json', sa.JSON(), nullable=True),
sa.Column('requires_attention', sa.Boolean(), nullable=False),
sa.Column('requires_attention', sa.Boolean(), server_default=sa.text('true'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('stored_name')
)
op.create_table('alerts',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('file_id', sa.String(length=36), nullable=False),
sa.Column('level', sa.String(length=50), nullable=False),
sa.Column('file_id', sa.Uuid(), nullable=False),
sa.Column('level', sa.Enum('critical', 'warning', 'info', name='alertlevel'), nullable=False),
sa.Column('message', sa.String(length=500), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['file_id'], ['files.id'], ),
sa.ForeignKeyConstraint(['file_id'], ['files.id'], ondelete="CASCADE"),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###

op.execute("""
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
""")

op.execute("""
CREATE TRIGGER update_files_modtime
BEFORE UPDATE ON files
FOR EACH ROW
EXECUTE FUNCTION update_modified_column();
""")


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.execute("DROP TRIGGER update_files_modtime ON users;")
op.execute("DROP FUNCTION update_modified_column();")
op.drop_table('alerts')
op.drop_table('files')
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ dependencies = [
"asyncpg>=0.30.0",
"celery[redis]>=5.6.3",
"fastapi>=0.135.3",
"greenlet>=3.3.2",
"pydantic>=2.12.5",
"pydantic-settings>=2.14.2",
"python-multipart>=0.0.20",
"sqlalchemy>=2.0.48",
"uvicorn>=0.42.0",
Expand Down
Loading