diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fef146085..95b30d88a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: blacken-docs - repo: https://github.com/asottile/reorder_python_imports.git - rev: v2.7.1 + rev: v3.0.1 hooks: - id: reorder-python-imports language_version: python3.10 @@ -60,7 +60,7 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.940 hooks: - id: mypy args: [--ignore-missing-imports] diff --git a/alembic/versions/2022-03-10_add_instance_and_drop_use_case_from_.py b/alembic/versions/2022-03-10_add_instance_and_drop_use_case_from_.py new file mode 100644 index 000000000..1dd959e74 --- /dev/null +++ b/alembic/versions/2022-03-10_add_instance_and_drop_use_case_from_.py @@ -0,0 +1,42 @@ +"""add_instance_and_drop_use_case_from_variables + +Revision ID: 7ed1ef8a6308 +Revises: 0a64fe57718d +Create Date: 2022-03-10 10:37:46.956974 + +""" +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +from alembic import op + + +revision = "7ed1ef8a6308" +down_revision = "0a64fe57718d" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "variables", + sa.Column( + "instance_id", + sa.Integer, + nullable=False, + ), + ) + op.drop_column("variables", "use_case_uuid") + + +def downgrade(): + op.add_column( + "variables", + sa.Column( + "use_case_uuid", + UUID, + sa.ForeignKey("use_cases.uuid", ondelete="CASCADE"), + nullable=False, + ), + ) + op.drop_column("variables", "instance_id") diff --git a/requirements-base.txt b/requirements-base.txt index 796643985..d538c7f57 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -1,5 +1,5 @@ # File generated automatically. Do not update it manually. # Update files in the requirements folder instead. -pip==22.0.3 -setuptools==60.8.2 +pip==22.0.4 +setuptools==60.9.3 wheel==0.37.1 diff --git a/requirements-dev.txt b/requirements-dev.txt index c625d3a0f..fc9a4f57e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,53 +8,53 @@ asgiref==3.5.0 astroid==2.9.3 attrs==21.4.0 black==22.1.0 -boto3==1.20.52 -botocore==1.23.52 +boto3==1.21.18 +botocore==1.24.18 callee==0.3.1 certifi==2021.10.8 cffi==1.15.0 cfgv==3.3.1 -charset-normalizer==2.0.11 -click==8.0.3 +charset-normalizer==2.0.12 +click==8.0.4 content-size-limit-asgi==0.1.5 cryptography==36.0.1 -decopatch==1.4.9 +decopatch==1.4.10 dependency-injector==4.38.0 distlib==0.3.4 factory-boy==3.2.1 -Faker==12.2.0 -fastapi==0.73.0 -filelock==3.4.2 -Flask==2.0.2 -freezegun==1.1.0 +Faker==13.3.2 +fastapi==0.75.0 +filelock==3.6.0 +Flask==2.0.3 +freezegun==1.2.0 funcy==1.17 greenlet==1.1.2 gunicorn==20.1.0 h11==0.13.0 -identify==2.4.9 +identify==2.4.11 idna==3.3 iniconfig==1.1.1 isort==5.10.1 -itsdangerous==2.0.1 +itsdangerous==2.1.1 Jinja2==3.0.3 jmespath==0.10.0 lazy-object-proxy==1.7.1 makefun==1.13.1 -Mako==1.1.6 -MarkupSafe==2.0.1 -marshmallow==3.14.1 +Mako==1.2.0 +MarkupSafe==2.1.0 +marshmallow==3.15.0 marshmallow-dataclass==8.5.3 marshmallow-enum==1.5.1 mccabe==0.6.1 more-itertools==8.12.0 -moto==3.0.3 -mypy==0.931 +moto==3.1.0 +mypy==0.940 mypy-extensions==0.4.3 nodeenv==1.6.0 packaging==21.3 pathspec==0.9.0 pika==1.2.0 -platformdirs==2.5.0 +platformdirs==2.5.1 pluggy==1.0.0 pre-commit==2.17.0 psycopg2-binary==2.9.3 @@ -63,8 +63,8 @@ pycparser==2.21 pydantic==1.9.0 pylint==2.12.2 pyparsing==3.0.7 -pytest==7.0.0 -pytest-cases==3.6.9 +pytest==7.1.0 +pytest-cases==3.6.10 pytest-freezegun==0.4.2 pytest-mock==3.7.0 pytest-pycharm==0.7.0 @@ -76,12 +76,12 @@ python-multipart==0.0.5 pytz==2021.3 PyYAML==6.0 requests==2.27.1 -responses==0.18.0 -s3transfer==0.5.1 -sentry-sdk==1.5.4 +responses==0.19.0 +s3transfer==0.5.2 +sentry-sdk==1.5.7 six==1.16.0 sniffio==1.2.0 -SQLAlchemy==1.4.31 +SQLAlchemy==1.4.32 sqlalchemy-repr==0.0.2 starlette==0.17.1 -e git+ssh://git@github.com/Destygo/pystatsd.git@c755b95da1f4692c49b8191f9ea0ef92f4c13c2c#egg=statsd @@ -89,10 +89,10 @@ toml==0.10.2 tomli==2.0.1 typeguard==2.13.3 typing-inspect==0.7.1 -typing_extensions==4.0.1 +typing_extensions==4.1.1 urllib3==1.26.8 -uvicorn==0.17.4 -virtualenv==20.13.1 +uvicorn==0.17.6 +virtualenv==20.13.3 Werkzeug==2.0.3 wrapt==1.13.3 xmltodict==0.12.0 diff --git a/requirements.txt b/requirements.txt index fc2284949..45b0429f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,45 +3,47 @@ alembic==1.7.6 anyio==3.5.0 asgiref==3.5.0 -boto3==1.20.52 -botocore==1.23.52 +boto3==1.21.18 +botocore==1.24.18 certifi==2021.10.8 -charset-normalizer==2.0.11 -click==8.0.3 +charset-normalizer==2.0.12 +click==8.0.4 content-size-limit-asgi==0.1.5 dependency-injector==4.38.0 -fastapi==0.73.0 +fastapi==0.75.0 funcy==1.17 greenlet==1.1.2 gunicorn==20.1.0 h11==0.13.0 idna==3.3 jmespath==0.10.0 -Mako==1.1.6 -MarkupSafe==2.0.1 -marshmallow==3.14.1 +Mako==1.2.0 +MarkupSafe==2.1.0 +marshmallow==3.15.0 marshmallow-dataclass==8.5.3 marshmallow-enum==1.5.1 more-itertools==8.12.0 mypy-extensions==0.4.3 +packaging==21.3 pika==1.2.0 psycopg2-binary==2.9.3 pydantic==1.9.0 +pyparsing==3.0.7 python-dateutil==2.8.2 python-dotenv==0.19.2 python-magic==0.4.25 python-multipart==0.0.5 requests==2.27.1 -s3transfer==0.5.1 -sentry-sdk==1.5.4 +s3transfer==0.5.2 +sentry-sdk==1.5.7 six==1.16.0 sniffio==1.2.0 -SQLAlchemy==1.4.31 +SQLAlchemy==1.4.32 sqlalchemy-repr==0.0.2 starlette==0.17.1 -e git+ssh://git@github.com/Destygo/pystatsd.git@c755b95da1f4692c49b8191f9ea0ef92f4c13c2c#egg=statsd typeguard==2.13.3 typing-inspect==0.7.1 -typing_extensions==4.0.1 +typing_extensions==4.1.1 urllib3==1.26.8 -uvicorn==0.17.4 +uvicorn==0.17.6 diff --git a/tests/api/namespace/test_variables.py b/tests/api/namespace/test_variables.py index 80d43239b..657f44546 100644 --- a/tests/api/namespace/test_variables.py +++ b/tests/api/namespace/test_variables.py @@ -1,5 +1,3 @@ -# pylint: disable=redefined-outer-name -import uuid from datetime import datetime from datetime import timedelta @@ -8,29 +6,23 @@ import pytest from tests import test_patterns -from tests.factories.use_case_factory import UseCaseFactory from tests.factories.variable_factory import VariableFactory from use_case_executor.adapters.database_repository.models.variable import Variable -@pytest.fixture -def creation_payload(): - return {"name": "My variable"} - - -@pytest.mark.parametrize("missing_key", ["name"]) -def test_create_fails_if_missing_key(missing_key, creation_payload, client, session): +@pytest.mark.parametrize("missing_key", ["instance_id", "name"]) +def test_create_fails_if_missing_key(missing_key, client, session): response = client.post( - f"/use_cases/{uuid.uuid4()}/variables", - json=funcy.omit(creation_payload, [missing_key]), + "/variables", + json=funcy.omit({"instance_id": 123, "name": "My variable"}, [missing_key]), ) assert response.status_code == 422 -@pytest.mark.parametrize("key,wrong_value", [("name", 123)]) +@pytest.mark.parametrize("key, wrong_value", [("instance_id", "abc"), ("name", 123)]) def test_create_fails_if_wrong_type_key(key, wrong_value, client, session): response = client.post( - f"/use_cases/{uuid.uuid4()}/variables", + "/variables", json=funcy.set_in( {"instance_id": 123, "name": "My variable"}, [key], wrong_value ), @@ -38,62 +30,57 @@ def test_create_fails_if_wrong_type_key(key, wrong_value, client, session): assert response.status_code == 422 -def test_create_fails_if_unknown_use_case(creation_payload, client, session): - response = client.post( - f"/use_cases/{uuid.uuid4()}/variables", - json=creation_payload, - ) - assert response.status_code == 404 - - -def test_create_variable_with_correct_parameters_works( - creation_payload, +def test_create_variable_with_correct_parameters_work( client, session, ): - use_case = UseCaseFactory() response = client.post( - f"/use_cases/{use_case.uuid}/variables", json=creation_payload + "/variables", json={"instance_id": 123, "name": "My variable"} ) assert response.status_code == 200 assert response.json() == { "uuid": callee.Regex(test_patterns.UUID_PATTERN), "name": "My variable", - "use_case_uuid": str(use_case.uuid), + "instance_id": 123, } variable = session.query(Variable).one() assert variable.uuid == callee.Regex(test_patterns.UUID_PATTERN) assert variable.name == "My variable" - assert variable.use_case_uuid == use_case.uuid + assert variable.instance_id == 123 now = datetime.utcnow() margin = timedelta(seconds=15) assert now - margin <= variable.created_at <= now -def test_list_variables_fails_if_unknown_use_case(creation_payload, session, client): - response = client.get( - f"/use_cases/{uuid.uuid4()}/variables", - ) - assert response.status_code == 404 +def test_list_variables_fails_if_no_instance_scope(session, client): + response = client.get("/variables") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["query", "instance_id"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } def test_list_variables_filters_by_instance(session, client): - v = VariableFactory() - VariableFactory() # variable on another use case + variable = VariableFactory(instance_id=123) + VariableFactory(instance_id=456) # decoy - response = client.get( - f"/use_cases/{v.use_case_uuid}/variables", - ) + response = client.get("/variables?instance_id=123") assert response.status_code == 200 assert response.json() == { "variables": [ { "uuid": callee.Regex(test_patterns.UUID_PATTERN), - "name": v.name, - "use_case_uuid": str(v.use_case_uuid), + "name": variable.name, + "instance_id": 123, } ] } diff --git a/tests/factories/variable_factory.py b/tests/factories/variable_factory.py index 9937ccca3..e9f5503a9 100644 --- a/tests/factories/variable_factory.py +++ b/tests/factories/variable_factory.py @@ -2,7 +2,6 @@ from factory import alchemy from tests import common -from tests.factories.use_case_factory import UseCaseFactory from use_case_executor.adapters.database_repository.models.variable import Variable @@ -12,5 +11,5 @@ class Meta: sqlalchemy_session = common.test_session sqlalchemy_session_persistence = alchemy.SESSION_PERSISTENCE_COMMIT - use_case = factory.SubFactory(UseCaseFactory) + instance_id = factory.fuzzy.FuzzyInteger(1, 3) name = factory.fuzzy.FuzzyText() diff --git a/use_case_executor/adapters/database_repository/models/use_case.py b/use_case_executor/adapters/database_repository/models/use_case.py index 3da3ca353..97937d718 100644 --- a/use_case_executor/adapters/database_repository/models/use_case.py +++ b/use_case_executor/adapters/database_repository/models/use_case.py @@ -8,7 +8,6 @@ from use_case_executor.adapters.database_repository.models.use_case_version import ( UseCaseVersion, ) -from use_case_executor.adapters.database_repository.models.variable import Variable from use_case_executor.domain.use_case.use_case import UseCase as DomainUseCase from use_case_executor.helpers import utils from use_case_executor.monitoring import monitored_funcs @@ -40,12 +39,6 @@ class UseCase(Base): foreign_keys=[UseCaseVersion.use_case_uuid], cascade="all,delete", ) - variables = relationship( - "Variable", - back_populates="use_case", - foreign_keys=[Variable.use_case_uuid], - cascade="all,delete", - ) @monitored_funcs.monitored_query("use_case_latest_version") def get_latest_version(self) -> Optional[UseCaseVersion]: diff --git a/use_case_executor/adapters/database_repository/models/variable.py b/use_case_executor/adapters/database_repository/models/variable.py index 938d19abf..5c0f43c1f 100644 --- a/use_case_executor/adapters/database_repository/models/variable.py +++ b/use_case_executor/adapters/database_repository/models/variable.py @@ -1,6 +1,5 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship from sqlalchemy.sql.functions import func from use_case_executor.adapters.database_repository.models.base import Base @@ -14,19 +13,13 @@ class Variable(Base): UUID, primary_key=True, server_default=sa.text("uuid_generate_v4()") ) created_at = sa.Column(sa.DateTime, nullable=False, server_default=func.now()) - use_case_uuid = sa.Column( - UUID(as_uuid=True), - sa.ForeignKey("use_cases.uuid", ondelete="CASCADE"), - nullable=False, - ) name = sa.Column(sa.String, nullable=False) - - use_case = relationship("UseCase", foreign_keys=[use_case_uuid]) + instance_id = sa.Column(sa.Integer, nullable=False) def to_domain_object(self) -> DomainVariable: return DomainVariable( uuid=self.uuid, name=self.name, - use_case_uuid=self.use_case_uuid, + instance_id=self.instance_id, created_at=self.created_at, ) diff --git a/use_case_executor/api/app_maker.py b/use_case_executor/api/app_maker.py index 5d21c04b2..7d0fde021 100644 --- a/use_case_executor/api/app_maker.py +++ b/use_case_executor/api/app_maker.py @@ -12,6 +12,7 @@ from use_case_executor.api.namespaces import image_upload from use_case_executor.api.namespaces import model_mappings from use_case_executor.api.namespaces import root +from use_case_executor.api.namespaces import variables from use_case_executor.api.namespaces.use_cases import use_cases from use_case_executor.monitoring import statsd_connector @@ -74,5 +75,6 @@ def create_app() -> FastAPI: fastapi_app.include_router(model_mappings.router) fastapi_app.include_router(root.router) fastapi_app.include_router(use_cases.router) + fastapi_app.include_router(variables.router) return fastapi_app diff --git a/use_case_executor/api/namespaces/use_cases/variables.py b/use_case_executor/api/namespaces/use_cases/variables.py deleted file mode 100644 index bf1f3c2b0..000000000 --- a/use_case_executor/api/namespaces/use_cases/variables.py +++ /dev/null @@ -1,72 +0,0 @@ -from uuid import UUID - -from fastapi import Depends -from pydantic import BaseModel -from pydantic import StrictStr -from pydantic import UUID4 -from sqlalchemy.orm import Session - -from use_case_executor.adapters.database_repository.models.variable import Variable -from use_case_executor.api.database_helpers import db_session -from use_case_executor.api.database_helpers import get_db_session -from use_case_executor.api.namespaces.use_cases import helpers -from use_case_executor.api.namespaces.use_cases.use_cases import router - - -class VariableModelIn(BaseModel): - name: StrictStr - - -class VariableModelOut(VariableModelIn): - uuid: UUID4 - use_case_uuid: UUID4 - - -class VariablesModelOut(BaseModel): - variables: list[VariableModelOut] - - -@router.post("/{use_case_uuid}/variables", response_model=VariableModelOut) -def create_variable( - use_case_uuid: UUID4, - variable_body: VariableModelIn, - db: Session = Depends(get_db_session), -) -> VariableModelOut: - db_session.set(db) - - use_case = helpers.get_use_case_or_raise_not_found_error( - use_case_uuid=use_case_uuid - ) - variable = Variable( - name=variable_body.name, - use_case=use_case, - ) - db.add(variable) - db.commit() - return VariableModelOut( - name=variable.name, use_case_uuid=variable.use_case_uuid, uuid=variable.uuid - ) - - -@router.get("/{use_case_uuid}/variables", response_model=VariablesModelOut) -def read_variables( - use_case_uuid: UUID4, - db: Session = Depends(get_db_session), -) -> VariablesModelOut: - db_session.set(db) - helpers.get_use_case_or_raise_not_found_error(use_case_uuid=use_case_uuid) - variables = _get_variables(session=db, use_case_uuid=use_case_uuid) - return VariablesModelOut( - variables=[ - VariableModelOut( - name=variable.name, - use_case_uuid=variable.use_case_uuid, - uuid=variable.uuid, - ) - for variable in variables - ] - ) - - -def _get_variables(session: Session, use_case_uuid: UUID) -> list[Variable]: - return session.query(Variable).filter(Variable.use_case_uuid == use_case_uuid).all() diff --git a/use_case_executor/api/namespaces/variables.py b/use_case_executor/api/namespaces/variables.py new file mode 100644 index 000000000..b15082722 --- /dev/null +++ b/use_case_executor/api/namespaces/variables.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter +from fastapi import Depends +from pydantic import BaseModel +from pydantic import StrictInt +from pydantic import StrictStr +from sqlalchemy.orm import Session + +from use_case_executor.adapters.database_repository.models.variable import Variable +from use_case_executor.api.database_helpers import db_session +from use_case_executor.api.database_helpers import get_db_session + +router = APIRouter(prefix="/variables", tags=["variables"]) + + +class VariableModelIn(BaseModel): + name: StrictStr + instance_id: StrictInt + + +class VariableModelOut(VariableModelIn): + uuid: StrictStr + + +class VariablesModelOut(BaseModel): + variables: list[VariableModelOut] + + +@router.post("", response_model=VariableModelOut) +def create_variable( + variable_body: VariableModelIn, + db: Session = Depends(get_db_session), +) -> VariableModelOut: + db_session.set(db) + variable = Variable( + name=variable_body.name, + instance_id=variable_body.instance_id, + ) + db.add(variable) + db.commit() + return VariableModelOut( + name=variable.name, instance_id=variable.instance_id, uuid=variable.uuid + ) + + +@router.get("", response_model=VariablesModelOut) +def read_variables( + instance_id: int, + db: Session = Depends(get_db_session), +) -> VariablesModelOut: + db_session.set(db) + variables = _get_variables(session=db, instance_id=instance_id) + return VariablesModelOut( + variables=[ + VariableModelOut( + name=variable.name, instance_id=variable.instance_id, uuid=variable.uuid + ) + for variable in variables + ] + ) + + +def _get_variables(session: Session, instance_id: int) -> list[Variable]: + return session.query(Variable).filter(Variable.instance_id == instance_id).all() diff --git a/use_case_executor/domain/variables/variable.py b/use_case_executor/domain/variables/variable.py index 2556ee764..aacf18e53 100644 --- a/use_case_executor/domain/variables/variable.py +++ b/use_case_executor/domain/variables/variable.py @@ -7,5 +7,5 @@ class Variable: uuid: UUID name: str - use_case_uuid: UUID + instance_id: int created_at: dt.datetime