From 65a56f26589707767587ea51aac3bf3451c58166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?c=C4=83t=C4=83lin?= Date: Sat, 9 Dec 2023 11:15:39 +0100 Subject: [PATCH] ci: add Dockerfile --- .gitignore | 1 + Dockerfile | 41 ++++ Makefile | 9 +- apps/http/chalice/.chalice/config.json | 8 - apps/http/chalice/app.py | 12 +- apps/http/litestar/app.py | 21 +- apps/http/sls/app.py | 2 +- apps/http/sls/serverless.yml | 54 +++-- pdm.lock | 262 ++++++++++++++++++++++++- pyproject.toml | 6 + secretsanta/domain.py | 9 +- secretsanta/repo.py | 8 +- secretsanta/service.py | 13 +- tests/unit/conftest.py | 2 +- tests/unit/test_service.py | 4 + 15 files changed, 382 insertions(+), 70 deletions(-) create mode 100644 Dockerfile delete mode 100644 apps/http/chalice/.chalice/config.json diff --git a/.gitignore b/.gitignore index ca1a5f2..a7e9c83 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ apps/http/sls/.serverless/ apps/http/sls/.requirements.zip __pycache__/ .pdm-python +.pdm-build/ .env .coverage htmlcov \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ad9b920 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV USERNAME "secretsanta" +ENV APP_HOME "/home/$USERNAME" +ENV APP_PATH "$APP_HOME/src" + +ARG uid=1000 +ARG gid=1000 + +# hadolint ignore=DL3001,DL3008 +RUN apt-get -y update \ + && apt-get -y upgrade \ + && apt-get -y install git curl make python3-virtualenv --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd -g "$gid" "$USERNAME" \ + && useradd -lu "$uid" -g "$gid" "$USERNAME" \ + && mkhomedir_helper "$USERNAME" \ + && mkdir "$APP_PATH" \ + && chown "$uid:$gid" "$APP_PATH" + +USER "$USERNAME" +ENV PATH="${PATH}:$APP_HOME/.local/bin" +ENV PYTHONPATH "$APP_PATH" + +RUN pip install pipx==1.2.0 --no-cache-dir \ + && pipx install pdm==2.10 + +WORKDIR "$APP_PATH" +ENV POETRY_VIRTUALENVS_IN_PROJECT=1 +COPY --chown="$USERNAME" pyproject.toml pdm.lock Makefile README.md LICENSE ./ +COPY --chown="$USERNAME" apps/ apps/ +COPY --chown="$USERNAME" secretsanta/ secretsanta/ + +RUN pdm install + +FROM base AS run + +RUN pdm install -G litestar +WORKDIR "$APP_PATH/apps/http/litestar" +CMD ["pdm", "run", "litestar", "run"] diff --git a/Makefile b/Makefile index 1e7596c..1d2452c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ +build: + docker build --target run -t git.roboces.dev/catalin/secretsanta:latest . + run--sls: @echo "Activating virtual environment and starting serverless offline" @. .venv/bin/activate && cd apps/http/sls && npx sls offline +run--litestar: + @echo "Starting litestar" + @. cd apps/http/litestar/ && pdm run litestar run + deploy--sls: @echo "Deploying via serverless framework" @. cd apps/http/sls && pdm export -G sls -f requirements --prod > requirements.txt && npx sls deploy @@ -11,4 +18,4 @@ lint: tests--unit: pdm run pytest --cov secretsanta tests - coverage html \ No newline at end of file + coverage html diff --git a/apps/http/chalice/.chalice/config.json b/apps/http/chalice/.chalice/config.json deleted file mode 100644 index 1bdea33..0000000 --- a/apps/http/chalice/.chalice/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": "2.0", - "app_name": "secretsanta", - "stages": { - "dev": { - } - } -} diff --git a/apps/http/chalice/app.py b/apps/http/chalice/app.py index 7e4f689..efaf700 100644 --- a/apps/http/chalice/app.py +++ b/apps/http/chalice/app.py @@ -4,8 +4,8 @@ from chalice import Chalice from secretsanta.domain import Group from secretsanta.repo import S3Repo from secretsanta.service import ( - GetGroupParticipantsService, CreateGroupService, + GetGroupParticipantsService, GetPairService, ) from secretsanta.settings import get_config @@ -16,13 +16,6 @@ app = Chalice(app_name="secretsanta", debug=config.environment == "local") repo = S3Repo() -def _participants2url(participants: list[str], group_uuid: str) -> list[str]: - return [ - f"{config.server_url}/api/v1/groups/{group_uuid}/pair/{participant_name}" - for participant_name in participants - ] - - @app.route("/api/v1/groups/{group_uuid}") def get_participants(group_uuid: str): service = GetGroupParticipantsService(repo) @@ -34,8 +27,7 @@ def create_group(group_uuid: str): request = app.current_request body = request.json_body service = CreateGroupService(repo) - participants = anyio.run(service.run, Group(uuid=group_uuid, **body)).participants - return _participants2url(participants, group_uuid) + return anyio.run(service.run, Group(uuid=group_uuid, **body)) @app.route("/api/v1/groups/{group_uuid}/pair/{participant}") diff --git a/apps/http/litestar/app.py b/apps/http/litestar/app.py index c053ca5..af06068 100644 --- a/apps/http/litestar/app.py +++ b/apps/http/litestar/app.py @@ -1,15 +1,15 @@ -from dataclasses import dataclass, asdict -from typing import Literal, Annotated +from dataclasses import asdict, dataclass +from typing import Annotated, Literal -from litestar import Controller, get, Litestar, put, Request, Response, MediaType +from litestar import Controller, Litestar, MediaType, Request, Response, get, put from litestar.params import Parameter from pydantic import UUID4 from secretsanta.domain import Group from secretsanta.repo import S3Repo from secretsanta.service import ( - GetGroupParticipantsService, CreateGroupService, + GetGroupParticipantsService, GetPairService, ) from secretsanta.settings import get_config @@ -46,10 +46,13 @@ class GroupController(Controller): @put("/{group_uuid:uuid}") async def create_group( - self, request: Request, group_uuid: UUID4, data: CreateGroup + self, + request: Request, + group_uuid: UUID4, + data: CreateGroup, ) -> list[str]: group = await self.create_group_service.run( - Group(uuid=group_uuid, **asdict(data)) + Group(uuid=group_uuid, **asdict(data)), ) return self._group2urls(group.participants, request.url) @@ -59,11 +62,13 @@ class GroupController(Controller): group_uuid: UUID4, participant_name: str, response_type: Annotated[ - Literal["json", "plain"], Parameter(query="type") + Literal["json", "plain"], + Parameter(query="type"), ] = "plain", ) -> Response[str] | PairResponse: pair = await self.get_pair_service.run( - group_uuid=group_uuid, participant=participant_name + group_uuid=group_uuid, + participant=participant_name, ) if response_type == "json": return PairResponse(pair=pair) diff --git a/apps/http/sls/app.py b/apps/http/sls/app.py index 4a267dc..3cf5e80 100644 --- a/apps/http/sls/app.py +++ b/apps/http/sls/app.py @@ -5,9 +5,9 @@ import anyio from secretsanta.domain import Group from secretsanta.repo import S3Repo from secretsanta.service import ( + CreateGroupService, GetGroupParticipantsService, GetPairService, - CreateGroupService, ) from secretsanta.settings import get_config diff --git a/apps/http/sls/serverless.yml b/apps/http/sls/serverless.yml index a496e21..7ac550d 100644 --- a/apps/http/sls/serverless.yml +++ b/apps/http/sls/serverless.yml @@ -26,16 +26,16 @@ custom: dockerizePip: true zip: true slim: true - layer: true - prune: - automatic: true - includeLayers: true - number: 1 - apiGatewayCaching: - enabled: true - apiGatewayThrottling: - maxRequestsPerSecond: 100 - maxConcurrentRequests: 5 + layer: false + #prune: + # automatic: true + # includeLayers: true + # number: 1 + #apiGatewayCaching: + # enabled: true + #apiGatewayThrottling: + # maxRequestsPerSecond: 100 + # maxConcurrentRequests: 5 plugins: - serverless-python-requirements @@ -47,27 +47,25 @@ plugins: functions: get_participants: handler: app.get_participants - layers: - - Ref: PythonRequirementsLambdaLayer events: - httpApi: path: /api/v1/groups/{group_uuid} method: get - create_group: - handler: app.create_group - layers: - - Ref: PythonRequirementsLambdaLayer - events: - - httpApi: - path: /api/v1/groups/{group_uuid} - method: put + #create_group: + # handler: app.create_group + # layers: + # - Ref: PythonRequirementsLambdaLayer + # events: + # - httpApi: + # path: /api/v1/groups/{group_uuid} + # method: put - get_pair: - handler: app.get_pair - layers: - - Ref: PythonRequirementsLambdaLayer - events: - - httpApi: - path: /api/v1/groups/{group_uuid}/pair/{participant} - method: get + #get_pair: + # handler: app.get_pair + # layers: + # - Ref: PythonRequirementsLambdaLayer + # events: + # - httpApi: + # path: /api/v1/groups/{group_uuid}/pair/{participant} + # method: get diff --git a/pdm.lock b/pdm.lock index d08c902..09934ad 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "litestar", "testing", "dev", "linting", "chalice", "sls"] strategy = ["cross_platform"] lock_version = "4.4" -content_hash = "sha256:3047d1e2fc131623e2ccfaab7af28560d897f225b61904fed245930b74f43c61" +content_hash = "sha256:ba9863870aa9f5f47435713bc9ae20992b801a13bced3f58aad28f2a5685614d" [[package]] name = "aioboto3" @@ -193,6 +193,36 @@ files = [ {file = "boto3-1.28.64.tar.gz", hash = "sha256:a5cf93b202568e9d378afdc84be55a6dedf11d30156289fe829e23e6d7dccabb"}, ] +[[package]] +name = "boto3-stubs" +version = "1.33.10" +requires_python = ">=3.7" +summary = "Type annotations for boto3 1.33.10 generated with mypy-boto3-builder 7.21.0" +dependencies = [ + "botocore-stubs", + "types-s3transfer", + "typing-extensions>=4.1.0; python_version < \"3.12\"", +] +files = [ + {file = "boto3-stubs-1.33.10.tar.gz", hash = "sha256:9e29024bcb6ac12c220d8ea5bfe79c532c92e0c1d1170c14217165e1a8b09218"}, + {file = "boto3_stubs-1.33.10-py3-none-any.whl", hash = "sha256:61d07f445f88d5cca0a66e23dfd83611ff947bac08e01a64908df3605c22aabc"}, +] + +[[package]] +name = "boto3-stubs" +version = "1.33.10" +extras = ["s3"] +requires_python = ">=3.7" +summary = "Type annotations for boto3 1.33.10 generated with mypy-boto3-builder 7.21.0" +dependencies = [ + "boto3-stubs==1.33.10", + "mypy-boto3-s3<1.34.0,>=1.33.0", +] +files = [ + {file = "boto3-stubs-1.33.10.tar.gz", hash = "sha256:9e29024bcb6ac12c220d8ea5bfe79c532c92e0c1d1170c14217165e1a8b09218"}, + {file = "boto3_stubs-1.33.10-py3-none-any.whl", hash = "sha256:61d07f445f88d5cca0a66e23dfd83611ff947bac08e01a64908df3605c22aabc"}, +] + [[package]] name = "botocore" version = "1.31.64" @@ -231,6 +261,39 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + [[package]] name = "chalice" version = "1.29.0" @@ -252,6 +315,46 @@ files = [ {file = "chalice-1.29.0.tar.gz", hash = "sha256:f07aad9a2d4e5b06ef279cf668a48ec455b131168000b91efe7d841425599035"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -339,6 +442,40 @@ files = [ {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] +[[package]] +name = "cryptography" +version = "41.0.7" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +dependencies = [ + "cffi>=1.12", +] +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + [[package]] name = "editorconfig" version = "0.12.3" @@ -648,6 +785,43 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "moto" +version = "4.2.11" +requires_python = ">=3.7" +summary = "" +dependencies = [ + "Jinja2>=2.10.1", + "boto3>=1.9.201", + "botocore>=1.12.201", + "cryptography>=3.3.1", + "python-dateutil<3.0.0,>=2.1", + "requests>=2.5", + "responses>=0.13.0", + "werkzeug!=2.2.0,!=2.2.1,>=0.5", + "xmltodict", +] +files = [ + {file = "moto-4.2.11-py2.py3-none-any.whl", hash = "sha256:58c12ab9ee69b6a5d1cddf83611ba4071508f07894317c57844b3ae6dc5bcd38"}, + {file = "moto-4.2.11.tar.gz", hash = "sha256:2da62d52eaa765dfe2762c920f0a88a58f3a09e04581c91db967d92faec848f1"}, +] + +[[package]] +name = "moto" +version = "4.2.11" +extras = ["s3"] +requires_python = ">=3.7" +summary = "" +dependencies = [ + "PyYAML>=5.1", + "moto==4.2.11", + "py-partiql-parser==0.4.2", +] +files = [ + {file = "moto-4.2.11-py2.py3-none-any.whl", hash = "sha256:58c12ab9ee69b6a5d1cddf83611ba4071508f07894317c57844b3ae6dc5bcd38"}, + {file = "moto-4.2.11.tar.gz", hash = "sha256:2da62d52eaa765dfe2762c920f0a88a58f3a09e04581c91db967d92faec848f1"}, +] + [[package]] name = "msgspec" version = "0.18.4" @@ -695,6 +869,19 @@ files = [ {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] +[[package]] +name = "mypy-boto3-s3" +version = "1.33.2" +requires_python = ">=3.7" +summary = "Type annotations for boto3.S3 1.33.2 service generated with mypy-boto3-builder 7.20.3" +dependencies = [ + "typing-extensions>=4.1.0; python_version < \"3.12\"", +] +files = [ + {file = "mypy-boto3-s3-1.33.2.tar.gz", hash = "sha256:f54a3ad3288f4e4719ebada3dde68c320507b0fc451d59bc68af7e6ab15cbdad"}, + {file = "mypy_boto3_s3-1.33.2-py3-none-any.whl", hash = "sha256:9d463df6def30de31a467d49ab92ff7795d46709d56eff6f52216a08bac27918"}, +] + [[package]] name = "packaging" version = "23.2" @@ -739,6 +926,25 @@ files = [ {file = "polyfactory-2.12.0.tar.gz", hash = "sha256:26dc3a52baae1ebd6386708d9a99f8ea4ef57c9d45e556815ee5e44a1cd27fc0"}, ] +[[package]] +name = "py-partiql-parser" +version = "0.4.2" +summary = "Pure Python PartiQL Parser" +files = [ + {file = "py-partiql-parser-0.4.2.tar.gz", hash = "sha256:9c99d545be7897c6bfa97a107f6cfbcd92e359d394e4f3b95430e6409e8dd1e1"}, + {file = "py_partiql_parser-0.4.2-py3-none-any.whl", hash = "sha256:f3f34de8dddf65ed2d47b4263560bbf97be1ecc6bd5c61da039ede90f26a10ce"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "C parser in Python" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "2.5.2" @@ -944,6 +1150,37 @@ files = [ {file = "readchar-4.0.5.tar.gz", hash = "sha256:08a456c2d7c1888cde3f4688b542621b676eb38cd6cfed7eb6cb2e2905ddc826"}, ] +[[package]] +name = "requests" +version = "2.31.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[[package]] +name = "responses" +version = "0.24.1" +requires_python = ">=3.8" +summary = "A utility library for mocking out the `requests` Python library." +dependencies = [ + "pyyaml", + "requests<3.0,>=2.30.0", + "urllib3<3.0,>=1.25.10", +] +files = [ + {file = "responses-0.24.1-py3-none-any.whl", hash = "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9"}, + {file = "responses-0.24.1.tar.gz", hash = "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c"}, +] + [[package]] name = "rich" version = "13.7.0" @@ -1301,6 +1538,19 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] +[[package]] +name = "werkzeug" +version = "3.0.1" +requires_python = ">=3.8" +summary = "The comprehensive WSGI web application library." +dependencies = [ + "MarkupSafe>=2.1.1", +] +files = [ + {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, + {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, +] + [[package]] name = "wheel" version = "0.42.0" @@ -1341,6 +1591,16 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "xmltodict" +version = "0.13.0" +requires_python = ">=3.4" +summary = "Makes working with XML feel like you are working with JSON" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + [[package]] name = "yarl" version = "1.9.4" diff --git a/pyproject.toml b/pyproject.toml index 9a4b1eb..f65c19f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ testing = [ "polyfactory>=2.12.0", "pytest>=7.4.3", "pytest-cov>=4.1.0", + "moto[s3]>=4.2.11", ] linting = [ "ruff>=0.1.7", @@ -41,4 +42,9 @@ build-backend = "pdm.backend" [tool.pdm.dev-dependencies] dev = [ "types-aioboto3-lite[s3]>=12.0.0", + "boto3-stubs[s3]>=1.33.10", ] + +[tool.ruff] +extend-select = ["W", "C90", "I", "N", "UP", "S", "BLE", "B", "A", "COM", "C4", "DTZ", "T10", "EM", "ISC", "T20", "PT", "RSE", "RET", "SIM", "PTH", "ERA", "PGH", "PL", "TRY", "RUF"] +extend-ignore = ["S101", "ISC002", "ISC002", "COM812"] \ No newline at end of file diff --git a/secretsanta/domain.py b/secretsanta/domain.py index 5807b88..2152ad6 100644 --- a/secretsanta/domain.py +++ b/secretsanta/domain.py @@ -1,7 +1,8 @@ -from abc import ABC, abstractmethod import random +from abc import ABC, abstractmethod +from typing import Any -from pydantic import BaseModel, UUID4 +from pydantic import UUID4, BaseModel from secretsanta.settings import Config, get_config @@ -42,3 +43,7 @@ class IService(ABC): def __init__(self, repo: IRepo, config: Config | None = None): self.repo = repo self.config = config or get_config() + + @abstractmethod + async def run(self, *args, **kwargs) -> Any: + ... # pragma: no cover diff --git a/secretsanta/repo.py b/secretsanta/repo.py index af7da1b..095f809 100644 --- a/secretsanta/repo.py +++ b/secretsanta/repo.py @@ -5,12 +5,12 @@ import aioboto3 from botocore.exceptions import ClientError from secretsanta.domain import Group, IRepo -from secretsanta.settings import get_config +from secretsanta.settings import Config, get_config class S3Repo(IRepo): - def __init__(self): - self.config = get_config() + def __init__(self, config: Config | None = None): + self.config = config or get_config() self.session = aioboto3.Session( aws_access_key_id=self.config.s3_access_key_id, aws_secret_access_key=self.config.s3_secret_access_key, @@ -34,7 +34,7 @@ class S3Repo(IRepo): Key=f"secretsanta/{group_uuid}.json", ) except ClientError: - return + return None else: group_raw = await s3_obj["Body"].read() return Group(**json.loads(group_raw)) diff --git a/secretsanta/service.py b/secretsanta/service.py index 7dd96e9..3f5028e 100644 --- a/secretsanta/service.py +++ b/secretsanta/service.py @@ -32,12 +32,13 @@ class CreateGroupService(IService): class GetGroupParticipantsService(IService): async def run(self, group_uuid: str) -> list[str] | None: group = await self.repo.get(group_uuid) - if group: - return participants2url( - participants=group.participants, - group_uuid=group.uuid, - server_url=self.config.server_url, - ) + if not group: + return None + return participants2url( + participants=group.participants, + group_uuid=group.uuid, + server_url=self.config.server_url, + ) class GetPairService(IService): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index aad45c2..188ab47 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -55,7 +55,7 @@ def repo_with_data(repo, group): @pytest.fixture() def config(): return get_config( - s3_bucket_name="foo", s3_secret_access_key="foo", s3_access_key_id="foo" + s3_bucket_name="foo", s3_secret_access_key="testing", s3_access_key_id="testing", s3_region="us-east-1" ) diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index e1c9e9d..ec499be 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -46,6 +46,10 @@ async def test_get_participants(get_participants_service, group, config): ) +async def test_get_participants_returns_none(get_participants_service, group): + assert not await get_participants_service.run(group.uuid) + + @pytest.mark.usefixtures("repo_with_data") async def test_get_pair(get_pair_service, group): assert group.pairs