From 0ee349d67c901985a0a620ad75437f497cc8da91 Mon Sep 17 00:00:00 2001 From: goddesseyes Date: Wed, 17 Jul 2024 02:23:36 +0300 Subject: [PATCH 1/2] Add GlossaryDocument & GlossaryRecord * rework db-connection, app-instance for test * add main.py for `fastapi dev` command * bump fastapi to 0.111 * rework settings instance --- backend/alembic/env.py | 5 +- .../versions/6d107741a92e_add_glossary.py | 55 +++++++ backend/app/__init__.py | 48 +++--- backend/app/auth.py | 3 +- backend/app/db.py | 30 +--- backend/app/glossary/__init__.py | 0 backend/app/glossary/models.py | 41 +++++ backend/app/routers/auth.py | 6 +- backend/app/schema.py | 13 +- backend/app/settings.py | 11 +- backend/app/translators/yandex.py | 6 +- backend/asgi.py | 3 - backend/main.py | 29 ++++ backend/requirements.txt | 2 +- backend/run.sh | 2 +- backend/tests/conftest.py | 63 ++++---- backend/tests/test_routes_auth.py | 29 ++-- backend/tests/test_routes_tmx.py | 49 +++--- backend/tests/test_routes_users.py | 15 +- backend/tests/test_routes_xliff.py | 80 +++++----- backend/tests/test_worker.py | 142 +++++++----------- 21 files changed, 357 insertions(+), 275 deletions(-) create mode 100644 backend/alembic/versions/6d107741a92e_add_glossary.py create mode 100644 backend/app/glossary/__init__.py create mode 100644 backend/app/glossary/models.py delete mode 100644 backend/asgi.py create mode 100644 backend/main.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 567e97c..2768769 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -16,8 +16,9 @@ # add your model's MetaData object here # for 'autogenerate' support -from app import schema -target_metadata = schema.Base.metadata +from app.db import Base + +target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/backend/alembic/versions/6d107741a92e_add_glossary.py b/backend/alembic/versions/6d107741a92e_add_glossary.py new file mode 100644 index 0000000..937c8ff --- /dev/null +++ b/backend/alembic/versions/6d107741a92e_add_glossary.py @@ -0,0 +1,55 @@ +"""add glossary + +Revision ID: 6d107741a92e +Revises: b3e764c93fac +Create Date: 2024-07-17 01:01:34.146777 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# pylint: disable=E1101 + +revision: str = "6d107741a92e" +down_revision: Union[str, None] = "b3e764c93fac" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "glossary_document", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_by", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["created_by"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "glossary_record", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("document_id", sa.Integer(), nullable=False), + sa.Column("comment", sa.String(), nullable=False), + sa.Column("src", sa.String(), nullable=False), + sa.Column("dst", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["document_id"], + ["glossary_document.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("glossary_record") + op.drop_table("glossary_document") diff --git a/backend/app/__init__.py b/backend/app/__init__.py index a4dcf79..cb74536 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,28 +1,22 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware +from app.db import Base +from app.glossary.models import GlossaryDocument, GlossaryRecord +from app.schema import ( + DocumentTask, + TmxDocument, + TmxRecord, + User, + XliffDocument, + XliffRecord, +) -from app.routers import auth, tmx, user, users, xliff - - -def create_app(): - app = FastAPI() - - # TODO: it would be nice to make it debug-only - origins = [ - "http://localhost:5173", - ] - app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - - app.include_router(auth.router) - app.include_router(tmx.router) - app.include_router(xliff.router) - app.include_router(user.router) - app.include_router(users.router) - - return app +__all__ = [ + "Base", + "DocumentTask", + "TmxDocument", + "TmxRecord", + "User", + "XliffDocument", + "XliffRecord", + "GlossaryDocument", + "GlossaryRecord", +] diff --git a/backend/app/auth.py b/backend/app/auth.py index 0cc03be..6e35a7b 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -6,11 +6,10 @@ from app import models, schema from app.db import get_db -from app.settings import Settings, get_settings +from app.settings import settings def get_current_user_id( - settings: Annotated[Settings, Depends(get_settings)], session: Annotated[str | None, Cookie(include_in_schema=False)] = None, ) -> int: if not session: diff --git a/backend/app/db.py b/backend/app/db.py index a0e1a9c..4b940c3 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -1,31 +1,17 @@ -from sqlalchemy import Engine, create_engine -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker -from app.settings import get_settings +from app.settings import settings -engine: Engine | None = None -SessionLocal: sessionmaker | None = None +engine = create_engine(settings.database_url) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -def init_connection(connection_url: str): - global engine, SessionLocal - engine = create_engine(connection_url) - SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - - -def close_connection(): - global engine - if engine: - engine.dispose() - engine = None +Base = declarative_base() def get_db(): - if not engine: - init_connection(get_settings().database_url) - - assert SessionLocal - db: Session = SessionLocal() + db = SessionLocal() try: yield db finally: diff --git a/backend/app/glossary/__init__.py b/backend/app/glossary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/glossary/models.py b/backend/app/glossary/models.py new file mode 100644 index 0000000..ab27e4f --- /dev/null +++ b/backend/app/glossary/models.py @@ -0,0 +1,41 @@ +from datetime import UTC, datetime +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.db import Base +from app.schema import User + +if TYPE_CHECKING: + from app.schema import User + + +class GlossaryDocument(Base): + __tablename__ = "glossary_document" + id: Mapped[int] = mapped_column(primary_key=True) + created_by: Mapped[int] = mapped_column(ForeignKey("user.id")) + created_at: Mapped[datetime] = mapped_column(default=datetime.now(UTC)) + updated_at: Mapped[datetime] = mapped_column(default=datetime.now(UTC)) + + records: Mapped[list["GlossaryRecord"]] = relationship( + back_populates="document", + cascade="all, delete-orphan", + order_by="GlossaryRecord.id", + ) + user: Mapped["User"] = relationship(back_populates="glossaries") + + +class GlossaryRecord(Base): + __tablename__ = "glossary_record" + + id: Mapped[int] = mapped_column(primary_key=True) + created_at: Mapped[datetime] = mapped_column(default=datetime.now(UTC)) + updated_at: Mapped[datetime] = mapped_column(default=datetime.now(UTC)) + + document_id: Mapped[int] = mapped_column(ForeignKey("glossary_document.id")) + comment: Mapped[str] = mapped_column() + src: Mapped[str] = mapped_column() + dst: Mapped[str] = mapped_column() + + document: Mapped["GlossaryDocument"] = relationship(back_populates="records") diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 15236c6..5051531 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,5 +1,4 @@ from datetime import UTC, datetime, timedelta -from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Response, status from itsdangerous import URLSafeTimedSerializer @@ -9,7 +8,7 @@ from app.auth import has_user_role from app.db import get_db from app.security import password_hasher -from app.settings import Settings, get_settings +from app.settings import settings router = APIRouter(prefix="/auth", tags=["auth"]) @@ -17,9 +16,8 @@ @router.post("/login") def login( data: models.AuthFields, - db: Annotated[Session, Depends(get_db)], - settings: Annotated[Settings, Depends(get_settings)], response: Response, + db: Session = Depends(get_db), ) -> models.StatusMessage: if not data.password: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) diff --git a/backend/app/schema.py b/backend/app/schema.py index 642f136..7614c34 100644 --- a/backend/app/schema.py +++ b/backend/app/schema.py @@ -1,11 +1,13 @@ from datetime import UTC, datetime +from typing import TYPE_CHECKING from sqlalchemy import ForeignKey -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship +from app.db import Base -class Base(DeclarativeBase): - pass +if TYPE_CHECKING: + from app.glossary.models import GlossaryDocument class TmxDocument(Base): @@ -90,3 +92,8 @@ class User(Base): xliffs: Mapped[list["XliffDocument"]] = relationship( back_populates="user", cascade="all, delete-orphan", order_by="XliffDocument.id" ) + glossaries: Mapped[list["GlossaryDocument"]] = relationship( + back_populates="user", + cascade="all, delete-orphan", + order_by="GlossaryDocument.id", + ) diff --git a/backend/app/settings.py b/backend/app/settings.py index c191c50..1abf74b 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -1,4 +1,4 @@ -from functools import lru_cache +from typing import Literal from pydantic_settings import BaseSettings @@ -11,8 +11,11 @@ class Settings(BaseSettings): translation_api: str = "https://translate.api.cloud.yandex.net" secret_key: str = "secret-key" domain_name: str | None = None + env: Literal["DEV", "PROD"] = "DEV" + origins: tuple[str, ...] = ( + "http://localhost:5173", + "http://localhost:8000", + ) -@lru_cache -def get_settings(): - return Settings() +settings = Settings() diff --git a/backend/app/translators/yandex.py b/backend/app/translators/yandex.py index 125ae39..19d7ba5 100644 --- a/backend/app/translators/yandex.py +++ b/backend/app/translators/yandex.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, PositiveInt, ValidationError from app.models import MachineTranslationSettings -from app.settings import get_settings +from app.settings import settings class YandexTranslatorResponse(BaseModel): @@ -59,7 +59,7 @@ def get_iam_token(oauth_token: str): iam_token (str): An IAM token from Yandex Translator API. """ response = requests.post( - f"{get_settings().iam_api}/iam/v1/tokens", + f"{settings.iam_api}/iam/v1/tokens", json={"yandexPassportOauthToken": oauth_token}, timeout=15, ) @@ -89,7 +89,7 @@ def translate_batch(lines: list[str], iam_token: str, folder_id: str) -> list[st } response = requests.post( - f"{get_settings().translation_api}/translate/v2/translate", + f"{settings.translation_api}/translate/v2/translate", json=json_data, headers=headers, timeout=15, diff --git a/backend/asgi.py b/backend/asgi.py deleted file mode 100644 index 0a23b5a..0000000 --- a/backend/asgi.py +++ /dev/null @@ -1,3 +0,0 @@ -from app import create_app - -app = create_app() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..4e531c0 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.routers import auth, tmx, user, users, xliff +from app.settings import settings + +ROUTERS = (auth, tmx, user, users, xliff) + + +def create_app(): + fastapi = FastAPI() + + fastapi.add_middleware( + CORSMiddleware, + allow_origins=settings.origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + register_routers(fastapi) + return fastapi + + +def register_routers(fastapi: FastAPI): + for router in ROUTERS: + fastapi.include_router(router.router) + + +app = create_app() diff --git a/backend/requirements.txt b/backend/requirements.txt index 905cd89..e414a3c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.110.* +fastapi==0.111.* python-multipart==0.0.9 httpx==0.26.0 pydantic_settings==2.2.1 diff --git a/backend/run.sh b/backend/run.sh index d633e62..0cbb87f 100644 --- a/backend/run.sh +++ b/backend/run.sh @@ -6,4 +6,4 @@ alembic upgrade head # then run the app -exec hypercorn -b 0.0.0.0:8000 --workers 4 --access-logfile - --error-logfile - asgi:app +exec hypercorn -b 0.0.0.0:8000 --workers 4 --access-logfile - --error-logfile - main:app diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 32ff661..92852e3 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,48 +1,59 @@ -import os -import tempfile -from contextlib import contextmanager - import pytest from fastapi.testclient import TestClient +from sqlalchemy import StaticPool, create_engine +from sqlalchemy.orm import Session, sessionmaker -from app import create_app, db, models, schema -from app.db import get_db, init_connection +from app import models, schema +from app.db import Base, get_db +from main import app -# pylint: disable=C0116 +SQLALCHEMY_DATABASE_URL = "sqlite:///" +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -@contextmanager -def session(): - return get_db() +Base.metadata.create_all(bind=engine) -@pytest.fixture() -def fastapi_app(): - db_fd, db_path = tempfile.mkstemp() +def override_get_db(): + try: + db = TestingSessionLocal() + yield db + finally: + db.close() + - app = create_app() - init_connection(f"sqlite:///{db_path}") - assert db.engine and db.SessionLocal +app.dependency_overrides[get_db] = override_get_db + + +@pytest.fixture() +def session(): + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) - schema.Base.metadata.drop_all(db.engine) - schema.Base.metadata.create_all(db.engine) + db = TestingSessionLocal() - yield app + try: + yield db + finally: + db.close() - db.close_connection() - os.close(db_fd) - os.unlink(db_path) +client = TestClient(app) @pytest.fixture() -def fastapi_client(fastapi_app): - yield TestClient(fastapi_app) +def fastapi_client(): + yield client @pytest.fixture() -def user_logged_client(fastapi_client: TestClient): - with session() as s: +def user_logged_client(fastapi_client: TestClient, session: Session): + with session as s: s.add( schema.User( username="test", diff --git a/backend/tests/test_routes_auth.py b/backend/tests/test_routes_auth.py index 5eef099..2c1da95 100644 --- a/backend/tests/test_routes_auth.py +++ b/backend/tests/test_routes_auth.py @@ -1,21 +1,14 @@ -from contextlib import contextmanager - import pytest from fastapi.testclient import TestClient +from sqlalchemy.orm import Session from app import models, schema -from app.db import get_db # pylint: disable=C0116 -@contextmanager -def session(): - return get_db() - - -def test_can_log_in(fastapi_client: TestClient): - with session() as s: +def test_can_log_in(fastapi_client: TestClient, session: Session): + with session as s: s.add( schema.User( username="test", @@ -36,8 +29,8 @@ def test_can_log_in(fastapi_client: TestClient): assert response.cookies["session"] -def test_can_log_in_with_remember(fastapi_client: TestClient): - with session() as s: +def test_can_log_in_with_remember(fastapi_client: TestClient, session: Session): + with session as s: s.add( schema.User( username="test", @@ -60,9 +53,9 @@ def test_can_log_in_with_remember(fastapi_client: TestClient): @pytest.mark.parametrize("password", ["1234", ""]) def test_returns_403_for_invalid_password( - fastapi_client: TestClient, password: str | None + fastapi_client: TestClient, password: str | None, session: Session ): - with session() as s: + with session as s: s.add( schema.User( username="test", @@ -81,8 +74,8 @@ def test_returns_403_for_invalid_password( assert response.status_code == 401 -def test_returns_403_for_disabled(fastapi_client: TestClient): - with session() as s: +def test_returns_403_for_disabled(fastapi_client: TestClient, session: Session): + with session as s: s.add( schema.User( username="test", @@ -110,8 +103,8 @@ def test_returns_403_for_disabled(fastapi_client: TestClient): assert response.status_code == 403 -def test_can_logout(fastapi_client: TestClient): - with session() as s: +def test_can_logout(fastapi_client: TestClient, session: Session): + with session as s: s.add( schema.User( username="test", diff --git a/backend/tests/test_routes_tmx.py b/backend/tests/test_routes_tmx.py index fdc06e6..dfbf731 100644 --- a/backend/tests/test_routes_tmx.py +++ b/backend/tests/test_routes_tmx.py @@ -1,21 +1,15 @@ -from contextlib import contextmanager from datetime import datetime from fastapi.testclient import TestClient +from sqlalchemy.orm import Session from app import schema -from app.db import get_db # pylint: disable=C0116 -@contextmanager -def session(): - return get_db() - - -def test_can_return_list_of_tmx_docs(user_logged_client: TestClient): - with session() as s: +def test_can_return_list_of_tmx_docs(user_logged_client: TestClient, session: Session): + with session as s: s.add(schema.TmxDocument(name="first_doc.tmx", created_by=1)) s.add(schema.TmxDocument(name="another_doc.tmx", created_by=1)) s.commit() @@ -36,17 +30,16 @@ def test_can_return_list_of_tmx_docs(user_logged_client: TestClient): ] -def test_can_get_tmx_file(user_logged_client: TestClient): +def test_can_get_tmx_file(user_logged_client: TestClient, session: Session): tmx_records = [ schema.TmxRecord(source="Regional Effects", target="Translation"), schema.TmxRecord(source="User Interface", target="UI"), ] - with session() as s: + with session as s: s.add( schema.TmxDocument(name="test_doc.tmx", records=tmx_records, created_by=1) ) s.commit() - response = user_logged_client.get("/tmx/1") assert response.status_code == 200 assert response.json() == { @@ -57,18 +50,18 @@ def test_can_get_tmx_file(user_logged_client: TestClient): } -def test_can_get_tmx_records(user_logged_client: TestClient): +def test_can_get_tmx_records(user_logged_client: TestClient, session: Session): tmx_records = [ schema.TmxRecord(source="Regional Effects", target="Translation"), schema.TmxRecord(source="User Interface", target="UI"), ] - with session() as s: + with session as s: s.add( schema.TmxDocument(name="test_doc.tmx", records=tmx_records, created_by=1) ) s.commit() - with session() as s: + with session as s: docs = s.query(schema.TmxDocument).all() assert len(docs) == 1 @@ -80,17 +73,19 @@ def test_can_get_tmx_records(user_logged_client: TestClient): ] -def test_can_get_tmx_records_with_page(user_logged_client: TestClient): +def test_can_get_tmx_records_with_page( + user_logged_client: TestClient, session: Session +): tmx_records = [ schema.TmxRecord(source=f"line{x}", target=f"line{x}") for x in range(150) ] - with session() as s: + with session as s: s.add( schema.TmxDocument(name="test_doc.tmx", records=tmx_records, created_by=1) ) s.commit() - with session() as s: + with session as s: docs = s.query(schema.TmxDocument).all() assert len(docs) == 1 @@ -100,17 +95,19 @@ def test_can_get_tmx_records_with_page(user_logged_client: TestClient): assert response.json()[0] == {"id": 101, "source": "line100", "target": "line100"} -def test_tmx_records_are_empty_for_too_large_page(user_logged_client: TestClient): +def test_tmx_records_are_empty_for_too_large_page( + user_logged_client: TestClient, session: Session +): tmx_records = [ schema.TmxRecord(source=f"line{x}", target=f"line{x}") for x in range(150) ] - with session() as s: + with session as s: s.add( schema.TmxDocument(name="test_doc.tmx", records=tmx_records, created_by=1) ) s.commit() - with session() as s: + with session as s: docs = s.query(schema.TmxDocument).all() assert len(docs) == 1 @@ -131,8 +128,8 @@ def test_returns_404_when_tmx_file_not_found(user_logged_client: TestClient): assert response.status_code == 404 -def test_can_delete_tmx_doc(user_logged_client: TestClient): - with session() as s: +def test_can_delete_tmx_doc(user_logged_client: TestClient, session: Session): + with session as s: s.add(schema.TmxDocument(name="first_doc.tmx", created_by=1)) s.commit() @@ -140,7 +137,7 @@ def test_can_delete_tmx_doc(user_logged_client: TestClient): assert response.status_code == 200 assert response.json() == {"message": "Deleted"} - with session() as s: + with session as s: doc = s.query(schema.TmxDocument).filter_by(id=1).first() assert doc is None @@ -150,7 +147,7 @@ def test_returns_404_when_deleting_nonexistent_tmx_doc(user_logged_client: TestC assert response.status_code == 404 -def test_can_upload_tmx(user_logged_client: TestClient): +def test_can_upload_tmx(user_logged_client: TestClient, session: Session): with open("tests/small.tmx", "rb") as f: response = user_logged_client.post( "/tmx", @@ -158,7 +155,7 @@ def test_can_upload_tmx(user_logged_client: TestClient): ) assert response.status_code == 200 - with session() as s: + with session as s: doc = s.query(schema.TmxDocument).filter_by(id=1).first() assert doc assert doc.name == "small.tmx" diff --git a/backend/tests/test_routes_users.py b/backend/tests/test_routes_users.py index bb0e132..87a3c08 100644 --- a/backend/tests/test_routes_users.py +++ b/backend/tests/test_routes_users.py @@ -1,19 +1,12 @@ -from contextlib import contextmanager - from fastapi.testclient import TestClient +from sqlalchemy.orm import Session from app import models -from app.db import get_db # pylint: disable=C0116 -@contextmanager -def session(): - return get_db() - - -def test_can_get_current_user(user_logged_client: TestClient): +def test_can_get_current_user(user_logged_client: TestClient, session: Session): response = user_logged_client.get("/user/") assert response.status_code == 200 assert response.json() == { @@ -25,6 +18,8 @@ def test_can_get_current_user(user_logged_client: TestClient): } -def test_cannot_get_current_user_for_non_logged_in(fastapi_client: TestClient): +def test_cannot_get_current_user_for_non_logged_in( + fastapi_client: TestClient, session: Session +): response = fastapi_client.get("/user/") assert response.status_code == 401 diff --git a/backend/tests/test_routes_xliff.py b/backend/tests/test_routes_xliff.py index cecd6a9..2f10724 100644 --- a/backend/tests/test_routes_xliff.py +++ b/backend/tests/test_routes_xliff.py @@ -1,22 +1,16 @@ import json -from contextlib import contextmanager from datetime import datetime, timedelta from fastapi.testclient import TestClient +from sqlalchemy.orm import Session from app import models, schema -from app.db import get_db # pylint: disable=C0116 -@contextmanager -def session(): - return get_db() - - -def test_can_get_list_of_xliff_docs(user_logged_client: TestClient): - with session() as s: +def test_can_get_list_of_xliff_docs(user_logged_client: TestClient, session: Session): + with session as s: s.add( schema.XliffDocument( name="first_doc.tmx", @@ -43,8 +37,8 @@ def test_can_get_list_of_xliff_docs(user_logged_client: TestClient): ] -def test_can_get_xliff_file(user_logged_client: TestClient): - with session() as s: +def test_can_get_xliff_file(user_logged_client: TestClient, session: Session): + with session as s: xliff_records = [ schema.XliffRecord( segment_id=8, @@ -83,8 +77,8 @@ def test_can_get_xliff_file(user_logged_client: TestClient): } -def test_can_get_xliff_records(user_logged_client: TestClient): - with session() as s: +def test_can_get_xliff_records(user_logged_client: TestClient, session: Session): + with session as s: xliff_records = [ schema.XliffRecord( segment_id=8, @@ -134,8 +128,10 @@ def test_can_get_xliff_records(user_logged_client: TestClient): ] -def test_xliff_records_returns_second_page(user_logged_client: TestClient): - with session() as s: +def test_xliff_records_returns_second_page( + user_logged_client: TestClient, session: Session +): + with session as s: xliff_records = [ schema.XliffRecord( segment_id=i, @@ -171,8 +167,10 @@ def test_xliff_records_returns_second_page(user_logged_client: TestClient): } -def test_xliff_records_returns_empty_for_too_large_page(user_logged_client: TestClient): - with session() as s: +def test_xliff_records_returns_empty_for_too_large_page( + user_logged_client: TestClient, session: Session +): + with session as s: xliff_records = [ schema.XliffRecord( segment_id=i, @@ -212,8 +210,8 @@ def test_returns_404_when_xliff_file_not_found(user_logged_client: TestClient): assert response.status_code == 404 -def test_can_update_xliff_record(user_logged_client: TestClient): - with session() as s: +def test_can_update_xliff_record(user_logged_client: TestClient, session: Session): + with session as s: xliff_records = [ schema.XliffRecord( segment_id=8, @@ -245,7 +243,7 @@ def test_can_update_xliff_record(user_logged_client: TestClient): assert response.status_code == 200 assert response.json() == {"message": "Record updated"} - with session() as s: + with session as s: record = s.query(schema.XliffRecord).filter(schema.XliffRecord.id == 2).one() assert record.target == "Updated" @@ -259,8 +257,10 @@ def test_returns_404_for_nonexistent_doc_when_updating_record( assert response.status_code == 404 -def test_returns_404_for_nonexistent_record(user_logged_client: TestClient): - with session() as s: +def test_returns_404_for_nonexistent_record( + user_logged_client: TestClient, session: Session +): + with session as s: s.add( schema.XliffDocument( name="test_doc.xliff", @@ -276,8 +276,8 @@ def test_returns_404_for_nonexistent_record(user_logged_client: TestClient): assert response.status_code == 404 -def test_can_delete_xliff_doc(user_logged_client: TestClient): - with session() as s: +def test_can_delete_xliff_doc(user_logged_client: TestClient, session: Session): + with session as s: s.add( schema.XliffDocument( name="first_doc.tmx", @@ -292,7 +292,7 @@ def test_can_delete_xliff_doc(user_logged_client: TestClient): assert response.status_code == 200 assert response.json() == {"message": "Deleted"} - with session() as s: + with session as s: assert s.query(schema.XliffDocument).count() == 0 @@ -303,12 +303,12 @@ def test_returns_404_when_deleting_nonexistent_xliff_doc( assert response.status_code == 404 -def test_upload(user_logged_client: TestClient): +def test_upload(user_logged_client: TestClient, session: Session): with open("tests/small.xliff", "rb") as fp: response = user_logged_client.post("/xliff", files={"file": fp}) assert response.status_code == 200 - with session() as s: + with session as s: doc = s.query(schema.XliffDocument).filter_by(id=1).first() assert doc is not None assert doc.name == "small.xliff" @@ -323,8 +323,8 @@ def test_upload_no_file(user_logged_client: TestClient): assert response.status_code == 422 -def test_upload_removes_old_files(user_logged_client: TestClient): - with session() as s: +def test_upload_removes_old_files(user_logged_client: TestClient, session: Session): + with session as s: s.add( schema.XliffDocument( name="some_doc.xliff", @@ -340,13 +340,15 @@ def test_upload_removes_old_files(user_logged_client: TestClient): response = user_logged_client.post("/xliff/", files={"file": fp}) assert response.status_code == 200 - with session() as s: + with session as s: doc = s.query(schema.XliffDocument).filter_by(name="some_doc.xliff").first() assert not doc -def test_upload_removes_only_uploaded_documents(user_logged_client: TestClient): - with session() as s: +def test_upload_removes_only_uploaded_documents( + user_logged_client: TestClient, session: Session +): + with session as s: s.add( schema.XliffDocument( name="uploaded_doc.xliff", @@ -371,7 +373,7 @@ def test_upload_removes_only_uploaded_documents(user_logged_client: TestClient): response = user_logged_client.post("/xliff/", files={"file": fp}) assert response.status_code == 200 - with session() as s: + with session as s: doc = s.query(schema.XliffDocument).filter_by(name="uploaded_doc.xliff").first() assert not doc doc = ( @@ -381,7 +383,7 @@ def test_upload_removes_only_uploaded_documents(user_logged_client: TestClient): def test_process_sets_document_in_pending_stage_and_creates_task( - user_logged_client: TestClient, + user_logged_client: TestClient, session: Session ): with open("tests/small.xliff", "rb") as fp: user_logged_client.post("/xliff/", files={"file": fp}) @@ -399,12 +401,12 @@ def test_process_sets_document_in_pending_stage_and_creates_task( assert response.status_code == 200 - with session() as s: + with session as s: doc = s.query(schema.XliffDocument).filter_by(id=1).one() assert doc.processing_status == "pending" -def test_process_creates_task(user_logged_client: TestClient): +def test_process_creates_task(user_logged_client: TestClient, session: Session): with open("tests/small.xliff", "rb") as fp: user_logged_client.post("/xliff/", files={"file": fp}) @@ -421,7 +423,7 @@ def test_process_creates_task(user_logged_client: TestClient): assert response.status_code == 200 - with session() as s: + with session as s: task = s.query(schema.DocumentTask).filter_by(id=1).one() assert task.status == "pending" loaded_data = json.loads(task.data) @@ -455,11 +457,11 @@ def test_returns_404_when_processing_nonexistent_xliff_doc( assert response.status_code == 404 -def test_download_xliff(user_logged_client: TestClient): +def test_download_xliff(user_logged_client: TestClient, session: Session): with open("tests/small.xliff", "rb") as fp: user_logged_client.post("/xliff", files={"file": fp}) - with session() as s: + with session as s: xliff_records = [ schema.XliffRecord( segment_id=675606, diff --git a/backend/tests/test_worker.py b/backend/tests/test_worker.py index 100dc02..e0626b5 100644 --- a/backend/tests/test_worker.py +++ b/backend/tests/test_worker.py @@ -1,53 +1,35 @@ import json -import os -import tempfile from datetime import datetime, timedelta import pytest from sqlalchemy.orm import Session -from app import db, models, schema -from app.db import get_db, init_connection +from app import models, schema +from app.db import get_db from worker import process_task # pylint: disable=C0116 -@pytest.fixture(autouse=True, scope="function") -def connection(): - db_fd, db_path = tempfile.mkstemp() - init_connection(f"sqlite:///{db_path}") - assert db.engine and db.SessionLocal - - schema.Base.metadata.drop_all(db.engine) - schema.Base.metadata.create_all(db.engine) - - yield - - db.close_connection() - os.close(db_fd) - os.unlink(db_path) - - def get_session() -> Session: return next(get_db()) -def test_process_task_sets_records(): +def test_process_task_sets_records(session: Session): with open("tests/small.xliff", "r", encoding="utf-8") as fp: file_data = fp.read() - with get_session() as session: + with session as s: tmx_records = [ schema.TmxRecord( source="Regional Effects", target="Translation", ) ] - session.add(schema.TmxDocument(name="test", records=tmx_records, created_by=1)) - session.commit() + s.add(schema.TmxDocument(name="test", records=tmx_records, created_by=1)) + s.commit() - session.add( + s.add( schema.XliffDocument( name="uploaded_doc.xliff", original_document=file_data, @@ -70,18 +52,18 @@ def test_process_task_sets_records(): } ), } - session.add( + s.add( schema.DocumentTask( data=json.dumps(task_data), status="pending", ) ) - session.commit() + s.commit() - result = process_task(session, session.query(schema.DocumentTask).one()) + result = process_task(s, s.query(schema.DocumentTask).one()) assert result - doc = session.query(schema.XliffDocument).filter_by(id=1).one() + doc = s.query(schema.XliffDocument).filter_by(id=1).one() assert doc.processing_status == "done" assert len(doc.records) == 4 # It provides text for matching TMX record @@ -118,11 +100,11 @@ def test_process_task_sets_records(): assert not doc.records[3].approved -def test_process_task_uses_correct_tmx_ids(): +def test_process_task_uses_correct_tmx_ids(session: Session): with open("tests/small.xliff", "r", encoding="utf-8") as fp: file_data = fp.read() - with get_session() as session: + with session as s: tmx_records_1 = [ schema.TmxRecord(source="Regional Effects", target="Translation"), schema.TmxRecord(source="Test", target="Segment"), @@ -130,15 +112,11 @@ def test_process_task_uses_correct_tmx_ids(): tmx_records_2 = [ schema.TmxRecord(source="Regional Effects", target="Another translation") ] - session.add( - schema.TmxDocument(name="test1", records=tmx_records_1, created_by=1) - ) - session.add( - schema.TmxDocument(name="test2", records=tmx_records_2, created_by=1) - ) - session.commit() + s.add(schema.TmxDocument(name="test1", records=tmx_records_1, created_by=1)) + s.add(schema.TmxDocument(name="test2", records=tmx_records_2, created_by=1)) + s.commit() - session.add( + s.add( schema.XliffDocument( name="uploaded_doc.xliff", original_document=file_data, @@ -161,18 +139,18 @@ def test_process_task_uses_correct_tmx_ids(): } ), } - session.add( + s.add( schema.DocumentTask( data=json.dumps(task_data), status="pending", ) ) - session.commit() + s.commit() - result = process_task(session, session.query(schema.DocumentTask).one()) + result = process_task(s, s.query(schema.DocumentTask).one()) assert result - doc = session.query(schema.XliffDocument).filter_by(id=1).one() + doc = s.query(schema.XliffDocument).filter_by(id=1).one() assert doc.processing_status == "done" assert len(doc.records) == 4 # It provides text for matching TMX record @@ -187,11 +165,11 @@ def test_process_task_uses_correct_tmx_ids(): ["mode", "trans_result"], [("newest", "Another translation"), ("oldest", "Translation")], ) -def test_process_task_uses_tmx_mode(mode: str, trans_result: str): +def test_process_task_uses_tmx_mode(mode: str, trans_result: str, session: Session): with open("tests/small.xliff", "r", encoding="utf-8") as fp: file_data = fp.read() - with get_session() as session: + with session as s: tmx_records_1 = [ schema.TmxRecord( source="Regional Effects", @@ -208,15 +186,11 @@ def test_process_task_uses_tmx_mode(mode: str, trans_result: str): change_date=datetime(2021, 1, 1, 0, 0, 0), ) ] - session.add( - schema.TmxDocument(name="test1", records=tmx_records_1, created_by=1) - ) - session.add( - schema.TmxDocument(name="test2", records=tmx_records_2, created_by=1) - ) - session.commit() + s.add(schema.TmxDocument(name="test1", records=tmx_records_1, created_by=1)) + s.add(schema.TmxDocument(name="test2", records=tmx_records_2, created_by=1)) + s.commit() - session.add( + s.add( schema.XliffDocument( name="uploaded_doc.xliff", original_document=file_data, @@ -239,33 +213,33 @@ def test_process_task_uses_tmx_mode(mode: str, trans_result: str): } ), } - session.add( + s.add( schema.DocumentTask( data=json.dumps(task_data), status="pending", ) ) - session.commit() + s.commit() - result = process_task(session, session.query(schema.DocumentTask).one()) + result = process_task(s, s.query(schema.DocumentTask).one()) assert result - doc = session.query(schema.XliffDocument).filter_by(id=1).one() + doc = s.query(schema.XliffDocument).filter_by(id=1).one() assert doc.processing_status == "done" assert len(doc.records) > 1 assert doc.records[0].target == trans_result -def test_process_task_substitutes_numbers(): +def test_process_task_substitutes_numbers(session: Session): with open("tests/small.xliff", "r", encoding="utf-8") as fp: file_data = fp.read() - with get_session() as session: + with session as s: tmx_records = [] - session.add(schema.TmxDocument(name="test", records=tmx_records, created_by=1)) - session.commit() + s.add(schema.TmxDocument(name="test", records=tmx_records, created_by=1)) + s.commit() - session.add( + s.add( schema.XliffDocument( name="uploaded_doc.xliff", original_document=file_data, @@ -288,18 +262,18 @@ def test_process_task_substitutes_numbers(): } ), } - session.add( + s.add( schema.DocumentTask( data=json.dumps(task_data), status="pending", ) ) - session.commit() + s.commit() - result = process_task(session, session.query(schema.DocumentTask).one()) + result = process_task(s, s.query(schema.DocumentTask).one()) assert result - doc = session.query(schema.XliffDocument).filter_by(id=1).one() + doc = s.query(schema.XliffDocument).filter_by(id=1).one() assert doc.processing_status == "done" assert len(doc.records) == 4 # It substitutes numbers @@ -310,8 +284,8 @@ def test_process_task_substitutes_numbers(): assert doc.records[3].target == "123456789" -def test_process_task_checks_task_data_attributes(): - with get_session() as session: +def test_process_task_checks_task_data_attributes(session: Session): + with session as s: datas = [ { "doc_id": 1, @@ -357,30 +331,30 @@ def test_process_task_checks_task_data_attributes(): ] for data in datas: - session.add(schema.DocumentTask(data=json.dumps(data), status="pending")) - session.commit() + s.add(schema.DocumentTask(data=json.dumps(data), status="pending")) + s.commit() - tasks = session.query(schema.DocumentTask).all() + tasks = s.query(schema.DocumentTask).all() for task in tasks: assert not process_task(session, task) -def test_process_task_deletes_task_after_processing(): - with get_session() as session: +def test_process_task_deletes_task_after_processing(session: Session): + with session as s: task = schema.DocumentTask(data=json.dumps({"doc_id": 1}), status="pending") - session.add(task) - session.commit() + s.add(task) + s.commit() - process_task(session, task) - assert not session.query(schema.DocumentTask).first() + process_task(s, task) + assert not s.query(schema.DocumentTask).first() -def test_process_task_puts_doc_in_error_state(monkeypatch): +def test_process_task_puts_doc_in_error_state(monkeypatch, session: Session): with open("tests/small.xliff", "r", encoding="utf-8") as fp: file_data = fp.read() - with get_session() as session: - session.add( + with session as s: + s.add( schema.XliffDocument( name="uploaded_doc.xliff", original_document=file_data, @@ -406,13 +380,13 @@ def test_process_task_puts_doc_in_error_state(monkeypatch): } ), } - session.add( + s.add( schema.DocumentTask( data=json.dumps(task_data), status="pending", ) ) - session.commit() + s.commit() def fake_translate(*args, **kwargs): raise RuntimeError() @@ -420,9 +394,9 @@ def fake_translate(*args, **kwargs): monkeypatch.setattr("app.translators.yandex.translate_lines", fake_translate) try: - process_task(session, session.query(schema.DocumentTask).one()) + process_task(s, s.query(schema.DocumentTask).one()) except AttributeError: pass - doc = session.query(schema.XliffDocument).filter_by(id=1).one() + doc = s.query(schema.XliffDocument).filter_by(id=1).one() assert doc.processing_status == "error" From 51e5ba3e61d0cfc15d9da97a93c8d10640481d66 Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Wed, 17 Jul 2024 20:29:30 +0300 Subject: [PATCH 2/2] Adjust column names for consistency --- backend/alembic/versions/6d107741a92e_add_glossary.py | 4 ++-- backend/app/db.py | 3 +-- backend/app/glossary/models.py | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/alembic/versions/6d107741a92e_add_glossary.py b/backend/alembic/versions/6d107741a92e_add_glossary.py index 937c8ff..380faf6 100644 --- a/backend/alembic/versions/6d107741a92e_add_glossary.py +++ b/backend/alembic/versions/6d107741a92e_add_glossary.py @@ -40,8 +40,8 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(), nullable=False), sa.Column("document_id", sa.Integer(), nullable=False), sa.Column("comment", sa.String(), nullable=False), - sa.Column("src", sa.String(), nullable=False), - sa.Column("dst", sa.String(), nullable=False), + sa.Column("source", sa.String(), nullable=False), + sa.Column("target", sa.String(), nullable=False), sa.ForeignKeyConstraint( ["document_id"], ["glossary_document.id"], diff --git a/backend/app/db.py b/backend/app/db.py index 4b940c3..15279c4 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -1,6 +1,5 @@ from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import declarative_base, sessionmaker from app.settings import settings diff --git a/backend/app/glossary/models.py b/backend/app/glossary/models.py index ab27e4f..bebea52 100644 --- a/backend/app/glossary/models.py +++ b/backend/app/glossary/models.py @@ -35,7 +35,7 @@ class GlossaryRecord(Base): document_id: Mapped[int] = mapped_column(ForeignKey("glossary_document.id")) comment: Mapped[str] = mapped_column() - src: Mapped[str] = mapped_column() - dst: Mapped[str] = mapped_column() + source: Mapped[str] = mapped_column() + target: Mapped[str] = mapped_column() document: Mapped["GlossaryDocument"] = relationship(back_populates="records")