From 04e1199ee95a803c2a7bc7e31697a2a322384fe9 Mon Sep 17 00:00:00 2001 From: DmsAnhr Date: Tue, 27 Jan 2026 09:11:58 +0700 Subject: [PATCH] Initial commit on BE --- .DS_Store | Bin 0 -> 8196 bytes .env.example | 14 + .gitignore | 174 + .pre-commit-config.yaml | 82 + .python-version | 1 + Dockerfile | 67 + README.md | 393 ++ alembic.ini | 106 + app/__init__.py | 0 app/api/dependencies/__init__.py | 0 app/api/dependencies/auth.py | 100 + app/api/dependencies/database.py | 0 app/api/dependencies/factory.py | 165 + app/api/v1/__init__.py | 40 + app/api/v1/routes/__init__.py | 37 + app/api/v1/routes/auth_route.py | 44 + app/api/v1/routes/category_route.py | 72 + app/api/v1/routes/classification_route.py | 78 + app/api/v1/routes/count_route.py | 14 + app/api/v1/routes/credential_route.py | 221 ++ app/api/v1/routes/feedback_route.py | 78 + app/api/v1/routes/file_route.py | 98 + app/api/v1/routes/geonetwork_route.py | 65 + .../v1/routes/map_projection_system_route.py | 87 + app/api/v1/routes/map_source_route.py | 72 + app/api/v1/routes/mapset_history_route.py | 55 + app/api/v1/routes/mapset_route.py | 136 + app/api/v1/routes/news_route.py | 75 + app/api/v1/routes/organization_route.py | 84 + app/api/v1/routes/regional_route.py | 68 + app/api/v1/routes/role_route.py | 62 + app/api/v1/routes/user_route.py | 94 + app/core/__init__.py | 0 app/core/config.py | 88 + app/core/data_types.py | 39 + app/core/database.py | 24 + app/core/exceptions.py | 63 + app/core/minio_client.py | 193 + app/core/params.py | 36 + app/core/responses.py | 16 + app/core/security.py | 80 + app/main.py | 117 + app/models/__init__.py | 38 + app/models/base.py | 12 + app/models/category_model.py | 16 + app/models/classification_model.py | 14 + app/models/credential_model.py | 28 + app/models/feedback_model.py | 23 + app/models/file_model.py | 31 + app/models/map_access_model.py | 45 + app/models/map_projection_system_model.py | 11 + app/models/map_source_model.py | 56 + app/models/mapset_history_model.py | 27 + app/models/mapset_model.py | 69 + app/models/news_model.py | 29 + app/models/organization_model.py | 42 + app/models/refresh_token_model.py | 23 + app/models/regional_model.py | 26 + app/models/role_model.py | 32 + app/models/user_model.py | 36 + app/repositories/__init__.py | 39 + app/repositories/base.py | 178 + app/repositories/category_repository.py | 143 + app/repositories/classification_repository.py | 8 + app/repositories/credential_repository.py | 120 + app/repositories/feedback_repository.py | 6 + app/repositories/file_repository.py | 16 + app/repositories/map_access_repository.py | 39 + .../map_projection_system_repository.py | 8 + app/repositories/map_source_repository.py | 8 + .../map_source_usage_repository.py | 28 + app/repositories/mapset_history_repository.py | 8 + app/repositories/mapset_repository.py | 258 ++ app/repositories/news_repository.py | 19 + app/repositories/organization_repository.py | 318 ++ app/repositories/regional_repository.py | 8 + app/repositories/role_repository.py | 24 + app/repositories/token_repository.py | 39 + app/repositories/user_repository.py | 41 + app/schemas/__init__.py | 80 + app/schemas/base.py | 28 + app/schemas/category_schema.py | 30 + app/schemas/classification_schema.py | 29 + app/schemas/count_schema.py | 9 + app/schemas/credential_schema.py | 212 ++ app/schemas/error_schema.py | 5 + app/schemas/feedback_schema.py | 34 + app/schemas/file_schema.py | 33 + app/schemas/map_access_schema.py | 30 + app/schemas/map_projection_system_schema.py | 20 + app/schemas/map_source_schema.py | 38 + app/schemas/mapset_history_schema.py | 24 + app/schemas/mapset_schema.py | 98 + app/schemas/news_schema.py | 29 + app/schemas/organization_schema.py | 57 + app/schemas/regional_schema.py | 32 + app/schemas/role_schema.py | 55 + app/schemas/token_schema.py | 21 + app/schemas/user_schema.py | 138 + app/services/__init__.py | 37 + app/services/auth_service.py | 83 + app/services/base.py | 204 + app/services/category_service.py | 9 + app/services/classification_service.py | 9 + app/services/count_service.py | 119 + app/services/credential_service.py | 303 ++ app/services/feedback_service.py | 8 + app/services/file_service.py | 164 + app/services/map_access_service.py | 24 + app/services/map_projection_system_service.py | 9 + app/services/map_source_service.py | 9 + app/services/mapset_history_service.py | 16 + app/services/mapset_service.py | 424 +++ app/services/news_service.py | 15 + app/services/organization_service.py | 78 + app/services/regional_service.py | 9 + app/services/role_service.py | 17 + app/services/user_service.py | 150 + app/utils/__init__.py | 0 app/utils/encryption.py | 66 + app/utils/helpers.py | 35 + app/utils/system.py | 37 + docker-compose.yml | 75 + environment.env | 12 + migrations/README | 1 + migrations/env.py | 79 + migrations/script.py.mako | 28 + migrations/scripts.py | 29 + .../versions/20241203_1200_initial_schema.py | 355 ++ .../20241204_0000_seed_initial_data.py | 114 + migrations/versions/__init__.py | 0 poetry.lock | 3300 +++++++++++++++++ pyproject.toml | 66 + run.py | 24 + tests/__init__.py | 0 tests/conftest.py | 0 tests/test_api/__init__.py | 0 tests/test_services/__init__.py | 0 138 files changed, 12014 insertions(+) create mode 100644 .DS_Store create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 alembic.ini create mode 100644 app/__init__.py create mode 100644 app/api/dependencies/__init__.py create mode 100644 app/api/dependencies/auth.py create mode 100644 app/api/dependencies/database.py create mode 100644 app/api/dependencies/factory.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/routes/__init__.py create mode 100644 app/api/v1/routes/auth_route.py create mode 100644 app/api/v1/routes/category_route.py create mode 100644 app/api/v1/routes/classification_route.py create mode 100644 app/api/v1/routes/count_route.py create mode 100644 app/api/v1/routes/credential_route.py create mode 100644 app/api/v1/routes/feedback_route.py create mode 100644 app/api/v1/routes/file_route.py create mode 100644 app/api/v1/routes/geonetwork_route.py create mode 100644 app/api/v1/routes/map_projection_system_route.py create mode 100644 app/api/v1/routes/map_source_route.py create mode 100644 app/api/v1/routes/mapset_history_route.py create mode 100644 app/api/v1/routes/mapset_route.py create mode 100644 app/api/v1/routes/news_route.py create mode 100644 app/api/v1/routes/organization_route.py create mode 100644 app/api/v1/routes/regional_route.py create mode 100644 app/api/v1/routes/role_route.py create mode 100644 app/api/v1/routes/user_route.py create mode 100644 app/core/__init__.py create mode 100644 app/core/config.py create mode 100644 app/core/data_types.py create mode 100644 app/core/database.py create mode 100644 app/core/exceptions.py create mode 100644 app/core/minio_client.py create mode 100644 app/core/params.py create mode 100644 app/core/responses.py create mode 100644 app/core/security.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/models/base.py create mode 100644 app/models/category_model.py create mode 100644 app/models/classification_model.py create mode 100644 app/models/credential_model.py create mode 100644 app/models/feedback_model.py create mode 100644 app/models/file_model.py create mode 100644 app/models/map_access_model.py create mode 100644 app/models/map_projection_system_model.py create mode 100644 app/models/map_source_model.py create mode 100644 app/models/mapset_history_model.py create mode 100644 app/models/mapset_model.py create mode 100644 app/models/news_model.py create mode 100644 app/models/organization_model.py create mode 100644 app/models/refresh_token_model.py create mode 100644 app/models/regional_model.py create mode 100644 app/models/role_model.py create mode 100644 app/models/user_model.py create mode 100644 app/repositories/__init__.py create mode 100644 app/repositories/base.py create mode 100644 app/repositories/category_repository.py create mode 100644 app/repositories/classification_repository.py create mode 100644 app/repositories/credential_repository.py create mode 100644 app/repositories/feedback_repository.py create mode 100644 app/repositories/file_repository.py create mode 100644 app/repositories/map_access_repository.py create mode 100644 app/repositories/map_projection_system_repository.py create mode 100644 app/repositories/map_source_repository.py create mode 100644 app/repositories/map_source_usage_repository.py create mode 100644 app/repositories/mapset_history_repository.py create mode 100644 app/repositories/mapset_repository.py create mode 100644 app/repositories/news_repository.py create mode 100644 app/repositories/organization_repository.py create mode 100644 app/repositories/regional_repository.py create mode 100644 app/repositories/role_repository.py create mode 100644 app/repositories/token_repository.py create mode 100644 app/repositories/user_repository.py create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/base.py create mode 100644 app/schemas/category_schema.py create mode 100644 app/schemas/classification_schema.py create mode 100644 app/schemas/count_schema.py create mode 100644 app/schemas/credential_schema.py create mode 100644 app/schemas/error_schema.py create mode 100644 app/schemas/feedback_schema.py create mode 100644 app/schemas/file_schema.py create mode 100644 app/schemas/map_access_schema.py create mode 100644 app/schemas/map_projection_system_schema.py create mode 100644 app/schemas/map_source_schema.py create mode 100644 app/schemas/mapset_history_schema.py create mode 100644 app/schemas/mapset_schema.py create mode 100644 app/schemas/news_schema.py create mode 100644 app/schemas/organization_schema.py create mode 100644 app/schemas/regional_schema.py create mode 100644 app/schemas/role_schema.py create mode 100644 app/schemas/token_schema.py create mode 100644 app/schemas/user_schema.py create mode 100644 app/services/__init__.py create mode 100644 app/services/auth_service.py create mode 100644 app/services/base.py create mode 100644 app/services/category_service.py create mode 100644 app/services/classification_service.py create mode 100644 app/services/count_service.py create mode 100644 app/services/credential_service.py create mode 100644 app/services/feedback_service.py create mode 100644 app/services/file_service.py create mode 100644 app/services/map_access_service.py create mode 100644 app/services/map_projection_system_service.py create mode 100644 app/services/map_source_service.py create mode 100644 app/services/mapset_history_service.py create mode 100644 app/services/mapset_service.py create mode 100644 app/services/news_service.py create mode 100644 app/services/organization_service.py create mode 100644 app/services/regional_service.py create mode 100644 app/services/role_service.py create mode 100644 app/services/user_service.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/encryption.py create mode 100644 app/utils/helpers.py create mode 100644 app/utils/system.py create mode 100644 docker-compose.yml create mode 100644 environment.env create mode 100644 migrations/README create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/scripts.py create mode 100644 migrations/versions/20241203_1200_initial_schema.py create mode 100644 migrations/versions/20241204_0000_seed_initial_data.py create mode 100644 migrations/versions/__init__.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 run.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api/__init__.py create mode 100644 tests/test_services/__init__.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9703f67d9c0adfa1a508eca8e888e87a19499c3a GIT binary patch literal 8196 zcmeI1!EO^V5QfKTfRZ9w!GRt?$_peFZ;)205E39n1xEy%gfx)su98jFy`bFT#ET$! zFz%e-8+%FCn^fFdBy7n$u|5CH_#b;6Cn92w=KY5v`y#SURk^prV?)t#t!t%L^xQ=n z@bY+^^rCSR(cW9r4jqAkz(8OiFc26B4EzfWaAs@SzTn(fm9o0LVJsw(!}0^+z_f1KBC$!by54#zBz|YKkj{DQ*tN!5Qxq`U@u=T#WUM zox-yzZYZX>c$lVD7t?VvJOcxPfprG-?p~L+3?!B_|Nd?su)RM2vc&>#{n4}XT;9q= zQW?vjg8K>XehGI2{h^E`lT(>XRMGx|_K%WwW2rp?6-y%1s)yPX%*VfB(kyy1pJnLE zBRP>=W<)R`<^y?!)iJS5XwPVOnM+1Jke9Rvc+}nrJw4=e+KIfUzlWBN%&h*L*$*tP zWpzhHyBceiMk8zK1KP3l(Gl4=OCo+&M7(hv+&$Pyl@VzXO@iNvW!AM~c}HuAPL9Wk zjD4=_USPO%kZ^9;*6`;oSNbd6W8m#H0mkbV3_==ZRt42*n~91`_mDOX#d zQb~O+o6+(rxC2iKHkGsKa;_*#q6VM!wTv)sNAlE z)yiyRLsvvK>zaXYr6_O83ppk)U3rRZgXsI-be(vJ>%=x^F1q&eithIc2iJ%~cNIIu z*R=Ur#o-R{aQ;JeD1m`1U|`Gn+}Hd6@x{OYUqNKSWndsMa9IqPdiSKuR~fymZ!CIL wH>q!^T2yc0q=oRXr#aGI$C1lF4EfwdRWY4HE}W!?5`GA(^b literal 0 HcmV?d00001 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d1dedca --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +DEBUG=1 +HOST=127.0.0.1 +PORT=5000 +DATABASE_URL=postgresql+asyncpg://satupeta_user:satupeta!QAZ2wsx@localhost:5432/satupeta +SECRET_KEY=tgAjg3fj4y4DtPCuIOlUH8cWFSxx9VZqvbXFsOiMnsmo2FF6NU +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +MINIO_ENDPOINT_URL=localhost:9000 +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin123 + +GEONETWORK_API_URL=https://geonetwork.jatimprov.go.id/geonetwork/srv/api/search/records/_search diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a19790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# 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/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +cover/ + +# 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 +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..905c197 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,82 @@ +repos: + # Formatting tools + - repo: https://github.com/psf/black + rev: 24.1.1 + hooks: + - id: black + args: + - "--line-length=119" + - "--include=\\.pyi?$" + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: + - --profile=black + + - repo: https://github.com/myint/autoflake + rev: v2.2.1 + hooks: + - id: autoflake + args: [ --in-place, --remove-unused-variables, --remove-all-unused-imports ] + files: \.py$ + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [ --py38-plus ] + + # Code quality and linting + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: fix-encoding-pragma + args: [ --remove ] + - id: check-yaml + - id: debug-statements + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-use-type-annotations + + + # Dependency management + - repo: https://github.com/peterdemin/pip-compile-multi + rev: v2.6.2 + hooks: + - id: pip-compile-multi-verify + + - repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.5.0 + hooks: + - id: setup-cfg-fmt + args: [ "--max-py-version=3.12" ] + + # Documentation and commit checks + - repo: local + hooks: + - id: rst-lint + name: rst + entry: rst-lint --encoding utf-8 + files: ^(RELEASING.rst|README.rst|TIDELIFT.rst)$ + language: python + + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.13.0 + hooks: + - id: commitizen + stages: [ commit-msg ] + + # Custom checks + - repo: local + hooks: + - id: pytest-staged + name: test on Staged + entry: sh -c 'pytest $(git diff --name-only --cached | grep -E "\\.py$") || exit 0' + stages: [ pre-commit ] + language: python diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..4eba2a6 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46ba47d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +FROM python:3.13-slim-bookworm AS base + +ENV POETRY_HOME="/opt/poetry" \ + PYTHONPATH=/app \ + PYTHONHASHSEED=0 \ + POETRY_VERSION=1.7.1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_VIRTUALENVS_IN_PROJECT=false \ + PYTHONWRITEBYTECODE=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATH="/opt/poetry/bin:$PATH" + +WORKDIR /app + +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + libpq-dev \ + locales \ + locales-all \ + libmagic1 \ + libjemalloc2 \ + procps && \ + rm -rf /var/lib/apt/lists/* && \ + echo "id_ID.UTF-8 UTF-8" > /etc/locale.gen && \ + locale-gen + +ENV LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libjemalloc.so.2" +ENV MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:30000,muzzy_decay_ms:30000" + +FROM base AS builder + +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + git \ + build-essential && \ + rm -rf /var/lib/apt/lists/* && \ + curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python3 - + +COPY pyproject.toml poetry.lock ./ +RUN --mount=type=cache,target=/root/.cache/pypoetry \ + poetry install --no-root --no-interaction --no-ansi + +RUN apt-get autoremove -y && \ + apt-get purge -y curl git build-essential && \ + apt-get clean -y && \ + rm -rf /root/.cache /var/lib/apt/lists/* + +FROM base AS app-image + +COPY --from=builder /opt/poetry /opt/poetry +COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ +COPY --from=builder /usr/local/bin/ /usr/local/bin/ + +COPY . /app + +ENV PYTHONFAULTHANDLER=1 \ + PYTHONHASHSEED=random \ + PYTHONOPTIMIZE=2 + +EXPOSE 5000 + +CMD ["python", "-OO", "run.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c359dfa --- /dev/null +++ b/README.md @@ -0,0 +1,393 @@ +# Portal Satu Peta Backend + +Backend for the Portal Satu Peta application. + +## Folder Structure + +``` +├── .env.example # Example environment file +├── .github/ # GitHub Actions configuration +│ └── workflows/ +│ └── deploy.yml # Workflow for deployment +├── .gitignore # Files and folders ignored by Git +├── .pre-commit-config.yaml # Pre-commit hooks configuration +├── Dockerfile # Instructions for building the Docker image +├── README.md # This file +├── alembic.ini # Alembic configuration for database migrations +├── app/ # Main application directory +│ ├── __init__.py +│ ├── api/ # API module (endpoints) +│ │ ├── dependencies/ # Dependencies for API (e.g., authentication) +│ │ └── v1/ # API version 1 +│ │ ├── __init__.py +│ │ └── routes/ # Route/endpoint definitions +│ ├── core/ # Core application configuration +│ │ ├── __init__.py +│ │ ├── config.py # Application settings (from environment variables) +│ │ ├── data_types.py # Custom data types +│ │ ├── database.py # Database configuration +│ │ ├── exceptions.py # Custom exceptions +│ │ ├── minio_client.py # Client for MinIO (object storage) +│ │ ├── params.py # Common parameters for requests +│ │ ├── responses.py # Standard response schemas +│ │ └── security.py # Security-related functions (password hashing, tokens) +│ ├── main.py # FastAPI application entry point +│ ├── models/ # SQLAlchemy model definitions (database tables) +│ │ ├── __init__.py +│ │ ├── base.py # Base model for SQLAlchemy +│ │ └── ... (other models) +│ ├── repositories/ # Data access logic (interaction with the database) +│ │ ├── __init__.py +│ │ ├── base.py # Base repository +│ │ └── ... (other repositories) +│ ├── schemas/ # Pydantic schemas (request/response data validation) +│ │ ├── __init__.py +│ │ ├── base.py # Base schema +│ │ └── ... (other schemas) +│ ├── services/ # Application business logic +│ │ ├── __init__.py +│ │ ├── base.py # Base service +│ │ └── ... (other services) +│ └── utils/ # General utilities +│ ├── __init__.py +│ ├── encryption.py # Encryption functions +│ ├── helpers.py # Helper functions +│ └── system.py # System-related utilities +├── assets/ # Static asset files (if any) +├── docker-compose.yml # Docker Compose configuration +├── migrations/ # Alembic database migration scripts +│ ├── README +│ ├── env.py +│ ├── script.py.mako +│ ├── scripts.py +│ └── versions/ # Migration version files +│ └── __init__.py +├── poetry.lock # Poetry dependency lock file +├── pyproject.toml # Poetry project configuration file +├── run.py # Script to run Uvicorn server locally +└── tests/ # Directory for unit and integration tests + ├── __init__.py + ├── conftest.py # Pytest configuration + ├── test_api/ + │ └── __init__.py + └── test_services/ + └── __init__.py +``` + +## How to Run the Project + +### 1. Initial Setup + +* Ensure you have Python (version >=3.10, <4.0 recommended as per `pyproject.toml`) and Poetry installed. +* Copy the `.env.example` file to `.env` and customize its configuration, especially for database and MinIO connections. + ```bash + cp .env.example .env + ``` +* Edit the `.env` file as needed. + +### 2. Running Locally (using Poetry and Uvicorn) + +1. **Install dependencies:** + ```bash + poetry install + ``` +2. **Run database migrations (if necessary):** + Ensure the database is running and the configuration in `.env` is correct. + ```bash + poetry run alembic upgrade head + ``` + Alternatively, if there's a custom script for migrations as seen in `deploy.yml`: + ```bash + poetry run python migrations/scripts.py + ``` + *(Check the content of `migrations/scripts.py` for the exact command if it differs)* + +3. **Run the application server:** + ```bash + poetry run python run.py + ``` + Or directly using Uvicorn: + ```bash + poetry run uvicorn app.main:app --host 0.0.0.0 --port 5000 --reload + ``` + The application will run at `http://localhost:5000` (or as configured in `.env` and `run.py`). + +### 3. Running Using Docker + +1. **Ensure Docker and Docker Compose are installed.** +2. **Build and run the container:** + From the project root directory, run: + ```bash + docker-compose up --build + ``` + If you have an `environment.env` file (as referenced in `docker-compose.yml`), ensure it exists and contains the necessary environment configurations. Otherwise, you might need to adjust `docker-compose.yml` to use the `.env` file or set environment variables directly. + + The application will run at `http://localhost:5000` (as per port mapping in `docker-compose.yml`). + +## How to Create an Endpoint, Model, Repository, and Service + +This project follows a layered architecture pattern commonly used in FastAPI applications. + +### 1. Creating a Model (`app/models/`) + +Models represent tables in your database. They are defined using SQLAlchemy. + +Example (e.g., `app/models/item_model.py`): +```python +from sqlalchemy import Column, Integer, String, ForeignKey, UUID +from sqlalchemy.orm import relationship +import uuid6 +from . import Base # Ensure Base is imported from app.models + +class ItemModel(Base): + __tablename__ = "items" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) # Example relationship + + owner = relationship("UserModel", back_populates="items") # Adjust to your User model +``` +* Don't forget to add the new model to `app/models/__init__.py` if necessary and create a database migration using Alembic. + ```bash + poetry run alembic revision -m "create_items_table" + ``` + Then edit the newly created migration file in `migrations/versions/` to define the `upgrade()` and `downgrade()` functions, and run: + ```bash + poetry run alembic upgrade head + ``` + +### 2. Creating a Schema (`app/schemas/`) + +Pydantic schemas are used for request data validation and response data formatting. + +Example (e.g., `app/schemas/item_schema.py`): +```python +from pydantic import BaseModel +from app.core.data_types import UUID7Field # Or the appropriate UUID type +from typing import Optional + +class ItemBase(BaseModel): + name: str + description: Optional[str] = None + +class ItemCreateSchema(ItemBase): + pass + +class ItemUpdateSchema(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + +class ItemSchema(ItemBase): + id: UUID7Field + owner_id: UUID7Field + + class Config: + orm_mode = True # or from_attributes = True for Pydantic v2 +``` + +### 3. Creating a Repository (`app/repositories/`) + +Repositories are responsible for all database interactions related to a model. + +Example (e.g., `app/repositories/item_repository.py`): +```python +from sqlalchemy import select +from fastapi_async_sqlalchemy import db # or the appropriate db session +from app.models.item_model import ItemModel # Import your model +from .base import BaseRepository # Import BaseRepository +from app.core.data_types import UUID7Field + +class ItemRepository(BaseRepository[ItemModel]): + def __init__(self): + super().__init__(ItemModel) + + async def find_by_name(self, name: str) -> ItemModel | None: + query = select(self.model).filter(self.model.name == name) + result = await db.session.execute(query) + return result.scalar_one_or_none() + + # Add other methods as needed (findById, create, update, delete, etc.) + # Example find_by_id from UserRepository: + async def find_by_id(self, id: UUID7Field) -> ItemModel | None: + query = select(self.model).filter(self.model.id == id) + result = await db.session.execute(query) + return result.scalar_one_or_none() +``` +* Ensure to register the new repository in `app/api/dependencies/factory.py` if you are using the factory pattern for dependencies. + +### 4. Creating a Service (`app/services/`) + +Services contain the application's business logic. Services will use repositories to interact with data. + +Example (e.g., `app/services/item_service.py`): +```python +from typing import Dict, List, Tuple, Union +from uuid6 import UUID # or from app.core.data_types import UUID7Field +from fastapi import HTTPException, status + +from app.models.item_model import ItemModel +from app.repositories.item_repository import ItemRepository +from app.schemas.item_schema import ItemCreateSchema, ItemUpdateSchema, ItemSchema # Import your schemas +from app.schemas.user_schema import UserSchema # For user info performing the action +from .base import BaseService +from app.core.exceptions import NotFoundException + +class ItemService(BaseService[ItemModel, ItemRepository]): + def __init__(self, repository: ItemRepository): + super().__init__(ItemModel, repository) + # self.user_service = user_service # If other services are needed + + async def create_item(self, item_data: ItemCreateSchema, current_user: UserSchema) -> ItemModel: + # Business logic before creating the item + # For example, check if an item with the same name already exists + existing_item = await self.repository.find_by_name(item_data.name) + if existing_item: + raise HTTPException(status_code=400, detail="Item with this name already exists") + + item_dict = item_data.model_dump() + item_dict['owner_id'] = current_user.id # Example of setting the owner + return await self.repository.create(item_dict) + + async def get_item_by_id(self, item_id: UUID, current_user: UserSchema) -> ItemModel: + item = await self.repository.find_by_id(item_id) + if not item: + raise NotFoundException(f"Item with id {item_id} not found") + # Business logic for authorization, for example: + # if item.owner_id != current_user.id and not current_user.is_admin: + # raise HTTPException(status_code=403, detail="Not authorized to access this item") + return item + + # Add other methods (update, delete, get_all, etc.) +``` +* Ensure to register the new service in `app/api/dependencies/factory.py`. + +### 5. Creating an Endpoint (`app/api/v1/routes/`) + +Endpoints are the HTTP entry points to your application. They are defined using FastAPI APIRouter. + +Example (e.g., `app/api/v1/routes/item_route.py`): +```python +from typing import List +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user # Authentication dependency +from app.api.dependencies.factory import Factory # Factory for service dependencies +from app.core.data_types import UUID7Field +from app.schemas.item_schema import ItemCreateSchema, ItemSchema, ItemUpdateSchema # Your schemas +from app.schemas.user_schema import UserSchema # User schema for auth dependency +from app.services.item_service import ItemService # Your service +from app.schemas.base import PaginatedResponse # If using pagination +from app.core.params import CommonParams # If using common parameters + +router = APIRouter() + +@router.post("/items", response_model=ItemSchema, status_code=status.HTTP_201_CREATED) +async def create_item( + item_in: ItemCreateSchema, + current_user: UserSchema = Depends(get_current_active_user), + service: ItemService = Depends(Factory().get_item_service), # Get service from factory +): + item = await service.create_item(item_data=item_in, current_user=current_user) + return item + +@router.get("/items/{item_id}", response_model=ItemSchema) +async def read_item( + item_id: UUID7Field, + current_user: UserSchema = Depends(get_current_active_user), + service: ItemService = Depends(Factory().get_item_service), +): + item = await service.get_item_by_id(item_id=item_id, current_user=current_user) + return item + +# Add other endpoints (GET all, PUT/PATCH, DELETE) +# Example GET all with pagination: +@router.get("/items", response_model=PaginatedResponse[ItemSchema]) +async def get_items( + params: CommonParams = Depends(), + user_active: UserSchema = Depends(get_current_active_user), + service: ItemService = Depends(Factory().get_item_service), +): + # Assume your service has a find_all method similar to UserService + items, total = await service.find_all( + filters=params.filter, + sort=params.sort, + search=params.search, + limit=params.limit, + offset=params.offset, + user=user_active, # For access control if needed + ) + + return PaginatedResponse( + items=[ItemSchema.model_validate(item) for item in items], + total=total, + limit=params.limit, + offset=params.offset, + has_more=total > (offset + params.limit), + ) + +``` +* Register the new router in `app/api/v1/__init__.py` or `app/main.py`. + Example in `app/main.py`: + ```python + // ... existing code ... + from app.api.v1.routes import item_route # Import your new router + // ... existing code ... + + app.include_router(item_route.router, prefix="/api/v1", tags=["Items"]) + // ... existing code ... + ``` + +## How to Deploy + +This project is configured to be deployed using GitHub Actions when there is a push to the `main` branch or via manual trigger. + +The deployment process defined in is as follows: + +1. **Checkout Code**: The code from the repository is fetched. +2. **Set up Docker Buildx**: Prepares the environment for building Docker images. +3. **Build and Export Docker image**: The Docker image `portal-satu-peta-backend:latest` is built and exported as a `.tar` file. + * Uses cache from GitHub Actions (GHA) to speed up the build process. +4. **Copy Docker image to server via SCP**: The `portal-satu-peta-backend.tar` file is copied to the target server (defined by secrets `SSH_HOST`, `SSH_USER`, `SSH_PORT`, `SSH_PASSWORD`). +5. **Deploy container**: An SSH script is executed on the target server: + * **Load image**: The Docker image from the `.tar` file is loaded into Docker on the server. + * **Stop and Remove Old Container**: The old container named `portal-satu-peta-backend` is stopped and removed (if it exists). + * **Run New Container**: A new container is run from the newly loaded image: + * Container name: `portal-satu-peta-backend` + * Restart policy: `unless-stopped` + * Environment file: `/home/application/.env` (ensure this file exists and is configured on the server) + * Port mapping: `5000:5000` (host port 5000 to container port 5000) + * Health check configuration. + * **Run Migrations**: The command `docker exec portal-satu-peta-backend python migrations/scripts.py` is executed inside the newly running container to perform database migrations. + * **Prune Old Images**: Old, unused Docker images (older than 24 hours) are removed to save disk space. + * **Clean Up**: The copied `.tar` file is removed from the server. + * **Verify Status**: The status of the `portal-satu-peta-backend` container is verified. + +### Server Requirements for Deployment: + +* Linux server with SSH access. +* Docker installed on the server. +* An environment file (e.g., `/home/application/.env`) must exist on the server and contain the correct configurations for production (database, secret keys, etc.). +* The SSH user used must have permissions to run Docker commands and access the necessary paths. + +### GitHub Secrets Configuration: + +Ensure the following secrets are configured in your GitHub repository (Settings > Secrets and variables > Actions): + +* `SSH_HOST`: IP address or hostname of the deployment server. +* `SSH_USER`: Username for SSH login to the server. +* `SSH_PORT`: SSH server port (usually 22). +* `SSH_PASSWORD`: Password for the SSH user. (Using SSH keys is highly recommended for security). + + + +# local run +- pyenv local 3.13.0 +- poetry env use python +- poetry install +- poetry install --no-root + + +# run +- poetry run uvicorn app.main:app --reload \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..53aad54 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,106 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = ${DATABASE_URL} + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/dependencies/__init__.py b/app/api/dependencies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/dependencies/auth.py b/app/api/dependencies/auth.py new file mode 100644 index 0000000..827c614 --- /dev/null +++ b/app/api/dependencies/auth.py @@ -0,0 +1,100 @@ +from datetime import datetime +from typing import Optional + +from fastapi import Depends, HTTPException, Request, status +from fastapi.security import OAuth2PasswordBearer +from fastapi.security.utils import get_authorization_scheme_param +from jose import JWTError +from pydantic import ValidationError +from pytz import timezone + +from app.api.dependencies.factory import Factory +from app.core.config import settings +from app.core.security import decode_token +from app.models import UserModel +from app.schemas.token_schema import TokenPayload +from app.schemas.user_schema import UserSchema +from app.services import UserService + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") + + +async def get_current_user( + token: str = Depends(oauth2_scheme), user_service: UserService = Depends(Factory().get_user_service) +) -> UserModel: + """Validate token and return current user.""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = decode_token(token) + token_data = TokenPayload(**payload) + + if token_data.type != "access": + raise credentials_exception + + user_id: Optional[str] = token_data.sub + if user_id is None: + raise credentials_exception + + except (JWTError, ValidationError): + raise credentials_exception + + user = await user_service.find_by_id(user_id) + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user") + + return user + + +async def get_payload(request: Request, user_service: UserService = Depends(Factory().get_user_service)): + authorization: str = request.headers.get("Authorization") + if not authorization: + return None + + scheme, token = get_authorization_scheme_param(authorization) + + if scheme.lower() != "bearer" or not token: + return None + + try: + payload = decode_token(token) + token_data = TokenPayload(**payload) + + user_id: Optional[str] = token_data.sub + if user_id is None: + return None + + user = await user_service.find_by_id(user_id) + if user is None: + return None + + return user + + except (JWTError, ValidationError): + return None + + +async def get_current_active_user(current_user: UserModel = Depends(get_current_user)) -> UserModel: + """Check if current user is active.""" + if not current_user.is_active or current_user.is_deleted: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user") + return current_user + + +async def get_current_active_admin(current_user: UserSchema = Depends(get_current_active_user)) -> UserModel: + """Check if current admin is active.""" + if current_user.role is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized") + if current_user.role.name != "administrator": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Your role does not have access to this resource" + ) + + return current_user diff --git a/app/api/dependencies/database.py b/app/api/dependencies/database.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/dependencies/factory.py b/app/api/dependencies/factory.py new file mode 100644 index 0000000..5009993 --- /dev/null +++ b/app/api/dependencies/factory.py @@ -0,0 +1,165 @@ +from functools import partial + +from app.core.minio_client import MinioClient +from app.models import ( + CategoryModel, + ClassificationModel, + CredentialModel, + FeedbackModel, + FileModel, + MapProjectionSystemModel, + MapsetHistoryModel, + MapsetModel, + MapSourceModel, + NewsModel, + OrganizationModel, + RefreshTokenModel, + RegionalModel, + RoleModel, + SourceUsageModel, + UserModel, +) +from app.repositories import ( + CategoryRepository, + ClassificationRepository, + CredentialRepository, + FeedbackRepository, + FileRepository, + MapProjectionSystemRepository, + MapsetHistoryRepository, + MapsetRepository, + MapSourceRepository, + NewsRepository, + OrganizationRepository, + RegionalRepository, + RoleRepository, + SourceUsageRepository, + TokenRepository, + UserRepository, +) +from app.services import ( + AuthService, + CategoryService, + CountService, + ClassificationService, + CredentialService, + FeedbackService, + FileService, + MapProjectionSystemService, + MapsetHistoryService, + MapsetService, + MapSourceService, + NewsService, + OrganizationService, + RegionalService, + RoleService, + UserService, +) + + +class Factory: + organization_repository = staticmethod(partial(OrganizationRepository, OrganizationModel, MapsetModel)) + role_repository = staticmethod(partial(RoleRepository, RoleModel)) + user_repository = staticmethod(partial(UserRepository, UserModel)) + token_repository = staticmethod(partial(TokenRepository, RefreshTokenModel)) + news_repository = staticmethod(partial(NewsRepository, NewsModel)) + file_repository = staticmethod(partial(FileRepository, FileModel)) + credential_repository = staticmethod(partial(CredentialRepository, CredentialModel)) + map_source_repository = staticmethod(partial(MapSourceRepository, MapSourceModel)) + map_projection_system_repository = staticmethod(partial(MapProjectionSystemRepository, MapProjectionSystemModel)) + category_repository = staticmethod(partial(CategoryRepository, CategoryModel)) + classification_repository = staticmethod(partial(ClassificationRepository, ClassificationModel)) + regional_repository = staticmethod(partial(RegionalRepository, RegionalModel)) + mapset_repository = staticmethod(partial(MapsetRepository, MapsetModel)) + mapset_history_repository = staticmethod(partial(MapsetHistoryRepository, MapsetHistoryModel)) + map_source_usage_repository = staticmethod(partial(SourceUsageRepository, SourceUsageModel)) + feedback_repository = staticmethod(partial(FeedbackRepository, FeedbackModel)) + + def get_auth_service( + self, + ): + return AuthService( + user_repository=self.user_repository(), + token_repository=self.token_repository(), + ) + + def get_organization_service( + self, + ): + return OrganizationService(self.organization_repository()) + + def get_role_service( + self, + ): + return RoleService(self.role_repository()) + + def get_user_service( + self, + ): + return UserService(self.user_repository(), self.role_repository()) + + def get_news_service( + self, + ): + return NewsService(self.news_repository()) + + def get_file_service( + self, + ): + return FileService(self.file_repository(), MinioClient()) + + def get_credential_service( + self, + ): + return CredentialService(self.credential_repository()) + + def get_map_source_service( + self, + ): + return MapSourceService(self.map_source_repository()) + + def get_map_projection_system_service( + self, + ): + return MapProjectionSystemService(self.map_projection_system_repository()) + + def get_category_service( + self, + ): + return CategoryService(self.category_repository()) + + def get_classification_service( + self, + ): + return ClassificationService(self.classification_repository()) + + def get_regional_service( + self, + ): + return RegionalService(self.regional_repository()) + + def get_mapset_service( + self, + ): + return MapsetService( + self.mapset_repository(), + self.mapset_history_repository(), + self.map_source_usage_repository(), + self.get_file_service(), + ) + + def get_mapset_history_service( + self, + ): + return MapsetHistoryService(self.mapset_history_repository()) + + def get_feedback_service( + self, + ): + return FeedbackService(self.feedback_repository()) + + def get_count_service( + self, + ): + # No repository dependencies needed for aggregated counts for now + return CountService() diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..081e01f --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter + +from app.api.v1.routes import ( + auth_router, + category_router, + count_router, + classification_router, + credential_router, + feedback_router, + file_router, + geonetwork_router, + map_projection_system_router, + map_source_router, + mapset_history_router, + mapset_router, + news_router, + organization_router, + regional_router, + role_router, + user_router, +) + +router = APIRouter() +router.include_router(auth_router, tags=["Auth"]) +router.include_router(category_router, tags=["Categories"]) +router.include_router(classification_router, tags=["Classifications"]) +router.include_router(credential_router, tags=["Credentials"]) +router.include_router(feedback_router, tags=["Feedback"]) +router.include_router(file_router, tags=["Files"]) +router.include_router(geonetwork_router, tags=["GeoNetwork"]) +router.include_router(count_router, tags=["Counts"]) +router.include_router(organization_router, tags=["Organizations"]) +router.include_router(map_source_router, tags=["Map Sources"]) +router.include_router(map_projection_system_router, tags=["Map Projection Systems"]) +router.include_router(mapset_router, tags=["Mapsets"]) +router.include_router(mapset_history_router, tags=["Mapset Histories"]) +router.include_router(news_router, tags=["News"]) +router.include_router(regional_router, tags=["Regionals"]) +router.include_router(role_router, tags=["Roles"]) +router.include_router(user_router, tags=["Users"]) diff --git a/app/api/v1/routes/__init__.py b/app/api/v1/routes/__init__.py new file mode 100644 index 0000000..69dcf67 --- /dev/null +++ b/app/api/v1/routes/__init__.py @@ -0,0 +1,37 @@ +from .auth_route import router as auth_router +from .category_route import router as category_router +from .classification_route import router as classification_router +from .credential_route import router as credential_router +from .feedback_route import router as feedback_router +from .file_route import router as file_router +from .count_route import router as count_router +from .geonetwork_route import router as geonetwork_router +from .map_projection_system_route import router as map_projection_system_router +from .map_source_route import router as map_source_router +from .mapset_history_route import router as mapset_history_router +from .mapset_route import router as mapset_router +from .news_route import router as news_router +from .organization_route import router as organization_router +from .regional_route import router as regional_router +from .role_route import router as role_router +from .user_route import router as user_router + +__all__ = [ + "organization_router", + "role_router", + "user_router", + "auth_router", + "news_router", + "file_router", + "credential_router", + "map_source_router", + "map_projection_system_router", + "category_router", + "regional_router", + "mapset_router", + "classification_router", + "mapset_history_router", + "feedback_router", + "geonetwork_router", + "count_router", +] diff --git a/app/api/v1/routes/auth_route.py b/app/api/v1/routes/auth_route.py new file mode 100644 index 0000000..b57bebb --- /dev/null +++ b/app/api/v1/routes/auth_route.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.schemas.token_schema import RefreshTokenSchema, Token +from app.schemas.user_schema import UserSchema +from app.services.auth_service import AuthService + +router = APIRouter() + + +@router.post("/auth/login", response_model=Token) +async def login( + form_data: OAuth2PasswordRequestForm = Depends(), auth_service: AuthService = Depends(Factory().get_auth_service) +): + user = await auth_service.authenticate_user(form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + return await auth_service.create_tokens(user.id) + + +@router.post("/auth/logout") +async def logout( + current_user: UserSchema = Depends(get_current_active_user), + auth_service: AuthService = Depends(Factory().get_auth_service), +): + await auth_service.logout(str(current_user.id)) + + +@router.post("/auth/refresh", response_model=Token) +async def refresh_token( + refresh_token: RefreshTokenSchema, auth_service: AuthService = Depends(Factory().get_auth_service) +): + return await auth_service.refresh_token(refresh_token.refresh_token) + + +@router.get("/me", response_model=UserSchema) +async def read_users_me(current_user: UserSchema = Depends(get_current_active_user)): + return current_user diff --git a/app/api/v1/routes/category_route.py b/app/api/v1/routes/category_route.py new file mode 100644 index 0000000..15a5bad --- /dev/null +++ b/app/api/v1/routes/category_route.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.category_schema import ( + CategoryCreateSchema, + CategorySchema, + CategoryUpdateSchema, +) +from app.services import CategoryService + +router = APIRouter() + + +@router.get("/categories", response_model=PaginatedResponse[CategorySchema]) +async def get_categorys( + params: CommonParams = Depends(), service: CategoryService = Depends(Factory().get_category_service) +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + categorys, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[CategorySchema.model_validate(category) for category in categorys], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/categories/{id}", response_model=CategorySchema) +async def get_category(id: UUID7Field, service: CategoryService = Depends(Factory().get_category_service)): + category = await service.find_by_id(id) + return category + + +@router.post( + "/categories", + response_model=CategorySchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_category( + data: CategoryCreateSchema, service: CategoryService = Depends(Factory().get_category_service) +): + category = await service.create(data.dict()) + return category + + +@router.patch("/categories/{id}", response_model=CategorySchema, dependencies=[Depends(get_current_active_user)]) +async def update_category( + id: UUID7Field, + data: CategoryUpdateSchema, + service: CategoryService = Depends(Factory().get_category_service), +): + category = await service.update(id, data.dict(exclude_unset=True)) + return category + + +@router.delete( + "/categories/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_category(id: UUID7Field, service: CategoryService = Depends(Factory().get_category_service)): + await service.delete(id) diff --git a/app/api/v1/routes/classification_route.py b/app/api/v1/routes/classification_route.py new file mode 100644 index 0000000..787284b --- /dev/null +++ b/app/api/v1/routes/classification_route.py @@ -0,0 +1,78 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas import ( + ClassificationCreateSchema, + ClassificationSchema, + ClassificationUpdateSchema, +) +from app.schemas.base import PaginatedResponse +from app.services import ClassificationService + +router = APIRouter() + + +@router.get("/classifications", response_model=PaginatedResponse[ClassificationSchema]) +async def get_classifications( + params: CommonParams = Depends(), service: ClassificationService = Depends(Factory().get_classification_service) +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + classifications, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[ClassificationSchema.model_validate(classification) for classification in classifications], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/classifications/{id}", response_model=ClassificationSchema) +async def get_classification( + id: UUID7Field, service: ClassificationService = Depends(Factory().get_classification_service) +): + classification = await service.find_by_id(id) + return classification + + +@router.post( + "/classifications", + response_model=ClassificationSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_classification( + data: ClassificationCreateSchema, service: ClassificationService = Depends(Factory().get_classification_service) +): + classification = await service.create(data.dict()) + return classification + + +@router.patch( + "/classifications/{id}", response_model=ClassificationSchema, dependencies=[Depends(get_current_active_user)] +) +async def update_classification( + id: UUID7Field, + data: ClassificationUpdateSchema, + service: ClassificationService = Depends(Factory().get_classification_service), +): + classification = await service.update(id, data.dict(exclude_unset=True)) + return classification + + +@router.delete( + "/classifications/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_classification( + id: UUID7Field, service: ClassificationService = Depends(Factory().get_classification_service) +): + await service.delete(id) diff --git a/app/api/v1/routes/count_route.py b/app/api/v1/routes/count_route.py new file mode 100644 index 0000000..a428b86 --- /dev/null +++ b/app/api/v1/routes/count_route.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, Depends + +from app.api.dependencies.factory import Factory +from app.schemas.count_schema import CountSchema +from app.services.count_service import CountService + + +router = APIRouter() + + +@router.get("/count", response_model=CountSchema) +async def get_counts(service: CountService = Depends(Factory().get_count_service)): + data = await service.get_counts() + return CountSchema(**data) diff --git a/app/api/v1/routes/credential_route.py b/app/api/v1/routes/credential_route.py new file mode 100644 index 0000000..632c354 --- /dev/null +++ b/app/api/v1/routes/credential_route.py @@ -0,0 +1,221 @@ +from typing import Optional +from uuid import UUID + +from fastapi import APIRouter, Depends, Path, Query, status + +from app.api.dependencies.auth import get_current_active_admin, get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.credential_schema import ( + CredentialCreateSchema, + CredentialSchema, + CredentialUpdateSchema, + CredentialWithSensitiveDataSchema, +) +from app.schemas.user_schema import UserSchema +from app.services import CredentialService + +router = APIRouter() + + +@router.post( + "/credentials", + summary="Buat kredensial baru", + status_code=status.HTTP_201_CREATED, +) +async def create_credential( + data: CredentialCreateSchema, + current_user: UserSchema = Depends(get_current_active_admin), + service: CredentialService = Depends(Factory().get_credential_service), +): + """ + Buat kredensial baru dengan data yang terenkripsi. + + Endpoint ini hanya dapat diakses oleh admin. + + **Data sensitive yang didukung:** + + - Database: + - host, port, username, password, database_name + - MinIO: + - endpoint, access_key, secret_key, secure, bucket_name + - API: + - base_url, api_key + - SSH: + - host, port, username, password (atau private_key) + - SMTP: + - host, port, username, password, use_tls + - FTP: + - host, port, username, password + """ + result = await service.create_credential( + name=data.name, + credential_type=data.credential_type, + sensitive_data=data.sensitive_data, + credential_metadata=data.credential_metadata, + description=data.description, + is_default=data.is_default, + user_id=current_user.id, + ) + return result + + +@router.get( + "/credentials", + response_model=PaginatedResponse[CredentialSchema], + summary="Dapatkan daftar kredensial", + dependencies=[Depends(get_current_active_user)], +) +async def get_credentials( + credential_type: Optional[str] = Query(None, description="Filter berdasarkan tipe kredensial"), + include_inactive: bool = Query(False, description="Sertakan kredensial yang tidak aktif"), + params: CommonParams = Depends(), + service: CredentialService = Depends(Factory().get_credential_service), +): + """ + Dapatkan daftar kredensial dengan filtering and pagination. + """ + filter_params = params.filter or [] + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + + if credential_type: + filter_params.append(f"credential_type={credential_type}") + + if not include_inactive: + filter_params.append(f"is_active=true") + + credentials, total = await service.find_all( + filters=filter_params, sort=sort, search=search, group_by=group_by, limit=limit, offset=offset + ) + + return PaginatedResponse( + items=[credential for credential in credentials], + total=total, + limit=limit, + offset=offset, + has_more=offset + limit < total, + ) + + +@router.get( + "/credentials/{credential_id}", + response_model=CredentialSchema, + summary="Dapatkan kredensial dengan data terdekripsi", + dependencies=[Depends(get_current_active_user)], +) +async def get_credential( + credential_id: UUID = Path(..., description="ID kredensial"), + service: CredentialService = Depends(Factory().get_credential_service), +): + """ + Dapatkan kredensial dengan data sensitif yang sudah didekripsi. + + Endpoint ini hanya dapat diakses oleh admin. + """ + credential = await service.find_by_id(credential_id) + + return credential + + +@router.get( + "/credentials/{credential_id}/decrypted", + response_model=CredentialWithSensitiveDataSchema, + summary="Dapatkan kredensial dengan data terdekripsi", + dependencies=[Depends(get_current_active_user)], +) +async def get_credential_decrypted( + credential_id: UUID = Path(..., description="ID kredensial"), + service: CredentialService = Depends(Factory().get_credential_service), +): + """ + Dapatkan kredensial dengan data sensitif yang sudah didekripsi. + + Endpoint ini hanya dapat diakses oleh admin. + """ + credential = await service.get_credential_with_decrypted_data(credential_id) + + return credential + + +@router.patch( + "/credentials/{credential_id}", + summary="Update kredensial", +) +async def update_credential( + credential_id: UUID, + data: CredentialUpdateSchema, + current_user: UserSchema = Depends(get_current_active_admin), + service: CredentialService = Depends(Factory().get_credential_service), +): + """ + Update kredensial. + + Data sensitif bisa diupdate secara parsial. Field yang tidak disebutkan + dalam data.sensitive_data tidak akan diubah. + + Endpoint ini hanya dapat diakses oleh admin. + """ + updated = await service.update_credential(credential_id, data.dict(exclude_unset=True), current_user.id) + return updated + + +@router.delete("/credentials/{credential_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_credential( + credential_id: UUID, + user: UserSchema = Depends(get_current_active_admin), + service: CredentialService = Depends(Factory().get_credential_service), +): + await service.delete(user, credential_id) + + +# @router.post( +# "/{credential_id}/test", +# response_model=CredentialTestResult, +# summary="Test koneksi menggunakan kredensial" +# ) +# async def test_credential( +# credential_id: UUID, +# current_user: UserSchema = Depends(get_current_active_user), +# service: CredentialService = Depends(Factory().get_credential_service) +# ): +# """ +# Test koneksi menggunakan kredensial. + +# Hasil test berisi flag sukses dan detail tambahan. +# """ +# result = await service.test_credential( +# service.db, credential_id, current_user.id +# ) +# return result + + +# @router.delete( +# "/{credential_id}", +# status_code=status.HTTP_204_NO_CONTENT, +# summary="Hapus kredensial" +# ) +# async def delete_credential( +# credential_id: UUID, +# current_user: UserSchema = Depends(get_current_admin_user), +# service: CredentialService = Depends(Factory().get_credential_service) +# ): +# """ +# Hapus kredensial secara permanen. + +# Endpoint ini hanya dapat diakses oleh admin. +# """ +# credential = await service.find_by_id(service.db, credential_id) +# if not credential: +# raise HTTPException( +# status_code=status.HTTP_404_NOT_FOUND, +# detail="Credential not found" +# ) + +# await service.delete(service.db, credential_id) + +# return None diff --git a/app/api/v1/routes/feedback_route.py b/app/api/v1/routes/feedback_route.py new file mode 100644 index 0000000..d2d133b --- /dev/null +++ b/app/api/v1/routes/feedback_route.py @@ -0,0 +1,78 @@ +from typing import List + +from fastapi import APIRouter, Body, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.feedback_schema import ( + FeedbackCreateSchema, + FeedbackSchema, + FeedbackUpdateSchema, +) +from app.services import FeedbackService + +router = APIRouter() + + +@router.get("/feedback", response_model=PaginatedResponse[FeedbackSchema]) +async def get_feedbacks( + params: CommonParams = Depends(), + service: FeedbackService = Depends(Factory().get_feedback_service), + user=Depends(get_current_active_user) +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + feedbacks, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[FeedbackSchema.model_validate(feedback) for feedback in feedbacks], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/feedback/{id}", response_model=FeedbackSchema) +async def get_feedback( + id: int, + service: FeedbackService = Depends(Factory().get_feedback_service), + user=Depends(get_current_active_user) +): + feedback = await service.find_by_id(id) + return feedback + + +@router.post("/feedback", response_model=FeedbackSchema, status_code=status.HTTP_201_CREATED) +async def create_feedback( + data: FeedbackCreateSchema, service: FeedbackService = Depends(Factory().get_feedback_service) +): + feedback = await service.create(data.dict()) + return feedback + + +@router.patch("/feedback/{id}", response_model=FeedbackSchema) +async def update_feedback( + id: int, + data: FeedbackUpdateSchema, + service: FeedbackService = Depends(Factory().get_feedback_service), + user=Depends(get_current_active_user) +): + feedback = await service.update(id, data.dict(exclude_unset=True)) + return feedback + + +@router.delete("/feedback/{id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_feedback( + id: int, + service: FeedbackService = Depends(Factory().get_feedback_service), + user=Depends(get_current_active_user) +): + await service.delete(id) diff --git a/app/api/v1/routes/file_route.py b/app/api/v1/routes/file_route.py new file mode 100644 index 0000000..8a09560 --- /dev/null +++ b/app/api/v1/routes/file_route.py @@ -0,0 +1,98 @@ +from typing import Optional + +from fastapi import ( + APIRouter, + Depends, + File, + Form, + HTTPException, + Response, + UploadFile, + status, +) +from fastapi.responses import StreamingResponse + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.models import UserModel +from app.schemas.base import PaginatedResponse +from app.schemas.file_schema import FileSchema +from app.services import FileService + +router = APIRouter() + + +@router.get("/files", response_model=PaginatedResponse[FileSchema], dependencies=[Depends(get_current_active_user)]) +async def get_files(params: CommonParams = Depends(), service: FileService = Depends(Factory().get_file_service)): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + files, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[FileSchema.model_validate(file) for file in files], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/files/{id}", response_model=FileSchema) +async def get_file(id: UUID7Field, service: FileService = Depends(Factory().get_file_service)): + file = await service.find_by_id(id) + return file + + +@router.post("/files", response_model=FileSchema, status_code=status.HTTP_201_CREATED) +async def upload_file( + file: UploadFile = File(...), + description: Optional[str] = Form(None), + current_user: UserModel = Depends(get_current_active_user), + service: FileService = Depends(Factory().get_file_service), +): + result = await service.upload_file(file=file, description=description, user_id=current_user.id) + return result + + +@router.get("/files/{file_id}", response_model=FileSchema, summary="Dapatkan metadata file") +async def get_file_info(file_id: UUID7Field, service: FileService = Depends(Factory().get_file_service)): + file = await service.find_by_id(file_id) + if not file: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File tidak ditemukan") + return file + + +@router.get("/files/{file_id}/download", summary="Download file") +async def download_file(file_id: UUID7Field, service: FileService = Depends(Factory().get_file_service)): + file_content, object_info, file_model = await service.get_file_content(file_id) + + async def iterfile(): + try: + chunk = await file_content.content.read(8192) + while chunk: + yield chunk + chunk = await file_content.content.read(8192) + finally: + await file_content.release() + + return StreamingResponse( + iterfile(), + media_type=file_model.content_type, + headers={"Content-Disposition": f'attachment; filename="{file_model.filename}"'}, + ) + + +@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Hapus file") +async def delete_file( + file_id: UUID7Field, + current_user: UserModel = Depends(get_current_active_user), + service: FileService = Depends(Factory().get_file_service), +): + await service.delete_file_with_content(file_id, str(current_user.id)) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/app/api/v1/routes/geonetwork_route.py b/app/api/v1/routes/geonetwork_route.py new file mode 100644 index 0000000..531588f --- /dev/null +++ b/app/api/v1/routes/geonetwork_route.py @@ -0,0 +1,65 @@ +from fastapi import APIRouter, HTTPException, status +import httpx + +router = APIRouter() + + +@router.get("/geonetwork-record") +async def get_geonetwork_record(): + """ + Proxy endpoint untuk mengambil data dari GeoNetwork Jatim. + Melakukan request POST ke API GeoNetwork dengan query default. + """ + geonetwork_url = "https://geonetwork.jatimprov.go.id/geonetwork/srv/api/search/records/_search" + + # Request body yang akan dikirim ke GeoNetwork API + request_body = { + "size": 0, + "track_total_hits": True, + "query": { + "bool": { + "must": { + "query_string": { + "query": "+isTemplate:n" + } + } + } + }, + "aggs": { + "resourceType": { + "terms": { + "field": "resourceType", + "size": 10 + } + } + } + } + + try: + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + geonetwork_url, + json=request_body, + headers={ + "Content-Type": "application/json", + "Accept": "application/json" + } + ) + response.raise_for_status() + return response.json() + + except httpx.TimeoutException: + raise HTTPException( + status_code=status.HTTP_504_GATEWAY_TIMEOUT, + detail="Request to GeoNetwork API timed out" + ) + except httpx.HTTPStatusError as e: + raise HTTPException( + status_code=e.response.status_code, + detail=f"GeoNetwork API returned error: {e.response.text}" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error connecting to GeoNetwork API: {str(e)}" + ) diff --git a/app/api/v1/routes/map_projection_system_route.py b/app/api/v1/routes/map_projection_system_route.py new file mode 100644 index 0000000..e257248 --- /dev/null +++ b/app/api/v1/routes/map_projection_system_route.py @@ -0,0 +1,87 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.map_projection_system_schema import ( + MapProjectionSystemCreateSchema, + MapProjectionSystemSchema, + MapProjectionSystemUpdateSchema, +) +from app.services import MapProjectionSystemService + +router = APIRouter() + + +@router.get("/map_projection_systems", response_model=PaginatedResponse[MapProjectionSystemSchema]) +async def get_map_projection_systems( + params: CommonParams = Depends(), + service: MapProjectionSystemService = Depends(Factory().get_map_projection_system_service), +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + map_projection_systems, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[ + MapProjectionSystemSchema.model_validate(map_projection_system) + for map_projection_system in map_projection_systems + ], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/map_projection_systems/{id}", response_model=MapProjectionSystemSchema) +async def get_map_projection_system( + id: UUID7Field, service: MapProjectionSystemService = Depends(Factory().get_map_projection_system_service) +): + map_projection_system = await service.find_by_id(id) + return map_projection_system + + +@router.post( + "/map_projection_systems", + response_model=MapProjectionSystemSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_map_projection_system( + data: MapProjectionSystemCreateSchema, + service: MapProjectionSystemService = Depends(Factory().get_map_projection_system_service), +): + map_projection_system = await service.create(data.dict()) + return map_projection_system + + +@router.patch( + "/map_projection_systems/{id}", + response_model=MapProjectionSystemSchema, + dependencies=[Depends(get_current_active_user)], +) +async def update_map_projection_system( + id: UUID7Field, + data: MapProjectionSystemUpdateSchema, + service: MapProjectionSystemService = Depends(Factory().get_map_projection_system_service), +): + map_projection_system = await service.update(id, data.dict(exclude_unset=True)) + return map_projection_system + + +@router.delete( + "/map_projection_systems/{id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(get_current_active_user)], +) +async def delete_map_projection_system( + id: UUID7Field, service: MapProjectionSystemService = Depends(Factory().get_map_projection_system_service) +): + await service.delete(id) diff --git a/app/api/v1/routes/map_source_route.py b/app/api/v1/routes/map_source_route.py new file mode 100644 index 0000000..18f2f1a --- /dev/null +++ b/app/api/v1/routes/map_source_route.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.map_source_schema import ( + MapSourceCreateSchema, + MapSourceSchema, + MapSourceUpdateSchema, +) +from app.services import MapSourceService + +router = APIRouter() + + +@router.get("/map_sources", response_model=PaginatedResponse[MapSourceSchema]) +async def get_mapSources( + params: CommonParams = Depends(), service: MapSourceService = Depends(Factory().get_map_source_service) +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + mapSources, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[MapSourceSchema.model_validate(mapSource) for mapSource in mapSources], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/map_sources/{id}", response_model=MapSourceSchema) +async def get_mapSource(id: UUID7Field, service: MapSourceService = Depends(Factory().get_map_source_service)): + mapSource = await service.find_by_id(id) + return mapSource + + +@router.post( + "/map_sources", + response_model=MapSourceSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_mapSource( + data: MapSourceCreateSchema, service: MapSourceService = Depends(Factory().get_map_source_service) +): + mapSource = await service.create(data.dict()) + return mapSource + + +@router.patch("/map_sources/{id}", response_model=MapSourceSchema, dependencies=[Depends(get_current_active_user)]) +async def update_mapSource( + id: UUID7Field, + data: MapSourceUpdateSchema, + service: MapSourceService = Depends(Factory().get_map_source_service), +): + mapSource = await service.update(id, data.dict(exclude_unset=True)) + return mapSource + + +@router.delete( + "/map_sources/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_mapSource(id: UUID7Field, service: MapSourceService = Depends(Factory().get_map_source_service)): + await service.delete(id) diff --git a/app/api/v1/routes/mapset_history_route.py b/app/api/v1/routes/mapset_history_route.py new file mode 100644 index 0000000..7f39c9a --- /dev/null +++ b/app/api/v1/routes/mapset_history_route.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas import MapsetHistoryCreateSchema, MapsetHistorySchema +from app.schemas.base import PaginatedResponse +from app.schemas.user_schema import UserSchema +from app.services import MapsetHistoryService + +router = APIRouter() + + +@router.get( + "/histories", + response_model=PaginatedResponse[MapsetHistorySchema], + dependencies=[Depends(get_current_active_user)], +) +async def get_mapset_histories( + params: CommonParams = Depends(), service: MapsetHistoryService = Depends(Factory().get_mapset_history_service) +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + histories, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[MapsetHistorySchema.model_validate(history) for history in histories], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.post("/histories", response_model=MapsetHistorySchema, status_code=status.HTTP_201_CREATED) +async def record_history( + data: MapsetHistoryCreateSchema, + user: UserSchema = Depends(get_current_active_user), + service: MapsetHistoryService = Depends(Factory().get_mapset_history_service), +): + return await service.create(user, data.dict()) + + +@router.delete( + "/histories/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_history( + id: UUID7Field, service: MapsetHistoryService = Depends(Factory().get_mapset_history_service) +): + await service.delete(id) diff --git a/app/api/v1/routes/mapset_route.py b/app/api/v1/routes/mapset_route.py new file mode 100644 index 0000000..9007188 --- /dev/null +++ b/app/api/v1/routes/mapset_route.py @@ -0,0 +1,136 @@ +from typing import List + +from fastapi import APIRouter, Body, Depends, status + +from app.api.dependencies.auth import get_current_active_user, get_payload +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.mapset_schema import ( + MapsetByOrganizationSchema, + MapsetCreateSchema, + MapsetSchema, + MapsetUpdateSchema, +) +from app.schemas.user_schema import UserSchema +from app.services import MapsetService + +router = APIRouter() + + +@router.get("/mapsets", response_model=PaginatedResponse[MapsetSchema]) +async def get_mapsets( + params: CommonParams = Depends(), + user: UserSchema = Depends(get_payload), + service: MapsetService = Depends(Factory().get_mapset_service), + landing: bool = False, +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + mapsets, total = await service.find_all(user, filter, sort, search, group_by, limit, offset, landing) + + return PaginatedResponse( + items=[MapsetSchema.model_validate(mapset) for mapset in mapsets], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/mapsets/organization", response_model=PaginatedResponse[MapsetByOrganizationSchema]) +async def get_mapsets_organization( + params: CommonParams = Depends(), + user: UserSchema = Depends(get_payload), + service: MapsetService = Depends(Factory().get_mapset_service), +): + filter = params.filter + sort = params.sort + search = params.search + limit = params.limit + offset = params.offset + mapsets, total = await service.find_all_group_by_organization(user, filter, sort, search, limit, offset) + return PaginatedResponse( + items=[MapsetByOrganizationSchema.model_validate(mapset) for mapset in mapsets], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/mapsets/{id}", response_model=MapsetSchema) +async def get_mapset( + id: UUID7Field, + user: UserSchema = Depends(get_payload), + service: MapsetService = Depends(Factory().get_mapset_service), +): + mapset = await service.find_by_id(id, user=user) + return mapset + + +@router.post("/mapsets", response_model=MapsetSchema, status_code=status.HTTP_201_CREATED) +async def create_mapset( + data: MapsetCreateSchema, + user: UserSchema = Depends(get_current_active_user), + service: MapsetService = Depends(Factory().get_mapset_service), +): + mapset = await service.create(user, data.dict()) + return mapset + + +@router.post("/mapsets/color_scale", status_code=status.HTTP_200_OK) +async def create_color_scale( + source_url: str = Body(..., embed=True), + color_range: list[str] = Body(None, embed=True), + boundary_file_id: UUID7Field = Body(None, embed=True), + service: MapsetService = Depends(Factory().get_mapset_service), +): + result, rangelist = await service.generate_colorscale(source_url, color_range, boundary_file_id) + return {"data": result, "rangelist": rangelist} + + +@router.patch( + "/mapsets/activation", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def update_mapset_activation( + ids: List[UUID7Field] = Body(...), + is_active: bool = Body(...), + service: MapsetService = Depends(Factory().get_mapset_service), +): + await service.bulk_update_activation(ids, is_active) + + +@router.patch("/mapsets/{id}", response_model=MapsetSchema, dependencies=[Depends(get_payload)]) +async def update_mapset( + id: UUID7Field, + data: MapsetUpdateSchema, + user: UserSchema = Depends(get_current_active_user), + service: MapsetService = Depends(Factory().get_mapset_service), +): + mapset = await service.update(id, user, data.dict(exclude_unset=True)) + return mapset + + +@router.delete( + "/mapsets/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_mapset(id: UUID7Field, service: MapsetService = Depends(Factory().get_mapset_service)): + await service.delete(id) + + +@router.post("/mapsets/{id}/download", status_code=status.HTTP_204_NO_CONTENT) +async def increment_download_mapset( + id: UUID7Field, + service: MapsetService = Depends(Factory().get_mapset_service), +): + """ + Increment download_count for the given mapset. + No file is downloaded; this only updates the counter. + """ + await service.increment_download_count(id) diff --git a/app/api/v1/routes/news_route.py b/app/api/v1/routes/news_route.py new file mode 100644 index 0000000..e655d16 --- /dev/null +++ b/app/api/v1/routes/news_route.py @@ -0,0 +1,75 @@ +from typing import List + +from fastapi import APIRouter, Body, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.news_schema import NewsCreateSchema, NewsSchema, NewsUpdateSchema +from app.services import NewsService + +router = APIRouter() + + +@router.get("/news", response_model=PaginatedResponse[NewsSchema]) +async def get_newss(params: CommonParams = Depends(), service: NewsService = Depends(Factory().get_news_service)): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + newss, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[NewsSchema.model_validate(news) for news in newss], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/news/{id}", response_model=NewsSchema) +async def get_news(id: UUID7Field, service: NewsService = Depends(Factory().get_news_service)): + news = await service.find_by_id(id) + return news + + +@router.post( + "/news", + response_model=NewsSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_news(data: NewsCreateSchema, service: NewsService = Depends(Factory().get_news_service)): + news = await service.create(data.dict()) + return news + + +@router.patch( + "/news/activation", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def update_news_activation( + ids: List[UUID7Field] = Body(...), + is_active: bool = Body(...), + service: NewsService = Depends(Factory().get_news_service), +): + await service.bulk_update_activation(ids, is_active) + + +@router.patch("/news/{id}", response_model=NewsSchema, dependencies=[Depends(get_current_active_user)]) +async def update_news( + id: UUID7Field, + data: NewsUpdateSchema, + service: NewsService = Depends(Factory().get_news_service), +): + news = await service.update(id, data.dict(exclude_unset=True)) + return news + + +@router.delete("/news/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)]) +async def delete_news(id: UUID7Field, service: NewsService = Depends(Factory().get_news_service)): + await service.delete(id) diff --git a/app/api/v1/routes/organization_route.py b/app/api/v1/routes/organization_route.py new file mode 100644 index 0000000..02a4595 --- /dev/null +++ b/app/api/v1/routes/organization_route.py @@ -0,0 +1,84 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user, get_payload +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.organization_schema import ( + OrganizationCreateSchema, + OrganizationSchema, + OrganizationUpdateSchema, +) +from app.schemas.user_schema import UserSchema +from app.services import OrganizationService + +router = APIRouter() + + +@router.get("/organizations", response_model=PaginatedResponse[OrganizationSchema]) +async def get_organizations( + params: CommonParams = Depends(), + user: UserSchema = Depends(get_payload), + service: OrganizationService = Depends(Factory().get_organization_service), + landing: bool = False, +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + organizations, total = await service.find_all(user, filter, sort, search, group_by, limit, offset, landing) + + return PaginatedResponse( + items=[OrganizationSchema.model_validate(organization) for organization in organizations], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/organizations/{id}", response_model=OrganizationSchema) +async def get_organization( + id: UUID7Field, + user: UserSchema = Depends(get_payload), + service: OrganizationService = Depends(Factory().get_organization_service), +): + organization = await service.get_organizations_by_id(user, id) + return organization + + +@router.post( + "/organizations", + response_model=OrganizationSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_organization( + data: OrganizationCreateSchema, service: OrganizationService = Depends(Factory().get_organization_service) +): + organization = await service.create(data.dict()) + return organization + + +@router.patch( + "/organizations/{id}", response_model=OrganizationSchema, dependencies=[Depends(get_current_active_user)] +) +async def update_organization( + id: UUID7Field, + data: OrganizationUpdateSchema, + service: OrganizationService = Depends(Factory().get_organization_service), +): + organization = await service.update(id, data.dict(exclude_unset=True)) + return organization + + +@router.delete( + "/organizations/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_organization( + id: UUID7Field, service: OrganizationService = Depends(Factory().get_organization_service) +): + await service.delete(id) diff --git a/app/api/v1/routes/regional_route.py b/app/api/v1/routes/regional_route.py new file mode 100644 index 0000000..35aeb57 --- /dev/null +++ b/app/api/v1/routes/regional_route.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas import RegionalCreateSchema, RegionalSchema, RegionalUpdateSchema +from app.schemas.base import PaginatedResponse +from app.services import RegionalService + +router = APIRouter() + + +@router.get("/regionals", response_model=PaginatedResponse[RegionalSchema]) +async def get_regionals( + params: CommonParams = Depends(), service: RegionalService = Depends(Factory().get_regional_service) +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + regionals, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[RegionalSchema.model_validate(regional) for regional in regionals], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/regionals/{id}", response_model=RegionalSchema) +async def get_regional(id: UUID7Field, service: RegionalService = Depends(Factory().get_regional_service)): + regional = await service.find_by_id(id) + return regional + + +@router.post( + "/regionals", + response_model=RegionalSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_regional( + data: RegionalCreateSchema, service: RegionalService = Depends(Factory().get_regional_service) +): + regional = await service.create(data.dict()) + return regional + + +@router.patch("/regionals/{id}", response_model=RegionalSchema, dependencies=[Depends(get_current_active_user)]) +async def update_regional( + id: UUID7Field, + data: RegionalUpdateSchema, + service: RegionalService = Depends(Factory().get_regional_service), +): + regional = await service.update(id, data.dict(exclude_unset=True)) + return regional + + +@router.delete( + "/regionals/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)] +) +async def delete_regional(id: UUID7Field, service: RegionalService = Depends(Factory().get_regional_service)): + await service.delete(id) diff --git a/app/api/v1/routes/role_route.py b/app/api/v1/routes/role_route.py new file mode 100644 index 0000000..b6caa89 --- /dev/null +++ b/app/api/v1/routes/role_route.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.role_schema import RoleCreateSchema, RoleSchema, RoleUpdateSchema +from app.services import RoleService + +router = APIRouter() + + +@router.get("/roles", response_model=PaginatedResponse[RoleSchema]) +async def get_roles(params: CommonParams = Depends(), service: RoleService = Depends(Factory().get_role_service)): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + roles, total = await service.find_all(filter, sort, search, group_by, limit, offset) + + return PaginatedResponse( + items=[RoleSchema.model_validate(role) for role in roles], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/roles/{id}", response_model=RoleSchema) +async def get_role(id: UUID7Field, service: RoleService = Depends(Factory().get_role_service)): + role = await service.find_by_id(id) + return role + + +@router.post( + "/roles", + response_model=RoleSchema, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_current_active_user)], +) +async def create_role(data: RoleCreateSchema, service: RoleService = Depends(Factory().get_role_service)): + role = await service.create(data.dict()) + return role + + +@router.patch("/roles/{id}", response_model=RoleSchema, dependencies=[Depends(get_current_active_user)]) +async def update_role( + id: UUID7Field, + data: RoleUpdateSchema, + service: RoleService = Depends(Factory().get_role_service), +): + role = await service.update(id, data.dict(exclude_unset=True)) + return role + + +@router.delete("/roles/{id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_active_user)]) +async def delete_role(id: UUID7Field, service: RoleService = Depends(Factory().get_role_service)): + await service.delete(id) diff --git a/app/api/v1/routes/user_route.py b/app/api/v1/routes/user_route.py new file mode 100644 index 0000000..d46327a --- /dev/null +++ b/app/api/v1/routes/user_route.py @@ -0,0 +1,94 @@ +from typing import List + +from fastapi import APIRouter, Body, Depends, status + +from app.api.dependencies.auth import get_current_active_user +from app.api.dependencies.factory import Factory +from app.core.data_types import UUID7Field +from app.core.params import CommonParams +from app.schemas.base import PaginatedResponse +from app.schemas.user_schema import UserCreateSchema, UserSchema, UserUpdateSchema +from app.services import UserService + +router = APIRouter() + + +@router.get("/users", response_model=PaginatedResponse[UserSchema]) +async def get_users( + params: CommonParams = Depends(), + user_active: UserSchema = Depends(get_current_active_user), + service: UserService = Depends(Factory().get_user_service), +): + filter = params.filter + sort = params.sort + search = params.search + group_by = params.group_by + limit = params.limit + offset = params.offset + users, total = await service.find_all( + filters=filter, + sort=sort, + search=search, + group_by=group_by, + limit=limit, + offset=offset, + user=user_active, + ) + + return PaginatedResponse( + items=[UserSchema.model_validate(user) for user in users], + total=total, + limit=limit, + offset=offset, + has_more=total > (offset + limit), + ) + + +@router.get("/users/{id}", response_model=UserSchema) +async def get_user( + id: UUID7Field, + user: UserSchema = Depends(get_current_active_user), + service: UserService = Depends(Factory().get_user_service), +): + user = await service.find_by_id(id, user) + return user + + +@router.post("/users", response_model=UserSchema, status_code=status.HTTP_201_CREATED) +async def create_user( + data: UserCreateSchema, + user: UserSchema = Depends(get_current_active_user), + service: UserService = Depends(Factory().get_user_service), +): + user = await service.create(data.dict(), user) + return user + + +@router.patch("/users/activation", status_code=status.HTTP_204_NO_CONTENT) +async def update_user_activation( + ids: List[UUID7Field] = Body(...), + is_active: bool = Body(...), + user: UserSchema = Depends(get_current_active_user), + service: UserService = Depends(Factory().get_user_service), +): + await service.bulk_update_activation(ids, is_active, user) + + +@router.patch("/users/{id}", response_model=UserSchema) +async def update_user( + id: UUID7Field, + data: UserUpdateSchema, + user: UserSchema = Depends(get_current_active_user), + service: UserService = Depends(Factory().get_user_service), +): + user = await service.update(id, data.dict(exclude_unset=True), user) + return user + + +@router.delete( + "/users/{id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(get_current_active_user)], +) +async def delete_user(id: UUID7Field, service: UserService = Depends(Factory().get_user_service)): + await service.delete(id) diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..5af032a --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,88 @@ +from functools import lru_cache +from typing import List, Optional + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + +from app.utils.system import get_optimal_workers + + +class Settings(BaseSettings): + # Application settings + PROJECT_NAME: str = Field(default="Satu Peta") + VERSION: str = Field(default="0.1.0") + DESCRIPTION: str = Field(default="Satu Peta API") + + # Server settings + DEBUG: bool = Field(default=False) + HOST: str = Field(default="127.0.0.1") + PORT: int = Field(default=8001) + WORKERS: int = Field(default=get_optimal_workers()) + LOG_LEVEL: str = Field(default="info") + LOOP: str = Field(default="uvloop") + HTTP: str = Field(default="httptools") + LIMIT_CONCURRENCY: int = Field(default=100) + BACKLOG: int = Field(default=2048) + LIMIT_MAX_REQUESTS: int | None = Field(default=None) + TIMEOUT_KEEP_ALIVE: int = Field(default=5) + H11_MAX_INCOMPLETE_EVENT_SIZE: int = Field(default=16 * 1024) + SERVER_HEADER: str = Field(default=f"{PROJECT_NAME}/{VERSION}") + FORWARDED_ALLOW_IPS: str = Field(default="*") + DATE_HEADER: bool = Field(default=True) + + @property + def ACCESS_LOG(self) -> bool: + return self.DEBUG + + # Database settings + DATABASE_URL: str + + # Security settings + SECRET_KEY: str + ALGORITHM: str = Field(default="HS256") + ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30) + REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7) + + # Cors settings + ALLOWED_ORIGINS: List[str] = Field(default=["*"]) + + # S3/MinIO settings + MINIO_ENDPOINT_URL: str = Field(default="http://localhost:9000") + MINIO_ROOT_USER: str + MINIO_ROOT_PASSWORD: str + MINIO_SECURE: Optional[bool] = False + MINIO_BUCKET_NAME: Optional[str] = Field(default="satu-peta") + MINIO_REGION: Optional[str] = Field(default=None) + + MAX_UPLOAD_SIZE: int = 100 * 1024 * 1024 # 100MB default limit + ALLOWED_EXTENSIONS: List[str] = [ + "jpg", + "jpeg", + "png", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "txt", + "csv", + "zip", + "rar", + "json", + ] + + TIMEZONE: str = Field(default="Asia/Jakarta") + + # GeoNetwork settings + GEONETWORK_API_URL: str = Field(default="https://geonetwork.jatimprov.go.id/geonetwork/srv/api/search/records/_search") + + # Settings config + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", case_sensitive=True, extra="allow") + + +@lru_cache +def get_settings() -> Settings: + return Settings() + + +settings = get_settings() diff --git a/app/core/data_types.py b/app/core/data_types.py new file mode 100644 index 0000000..15e8c0c --- /dev/null +++ b/app/core/data_types.py @@ -0,0 +1,39 @@ +from typing import Any + +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, PydanticCustomError +from pydantic_core.core_schema import ( + is_instance_schema, + json_or_python_schema, + no_info_plain_validator_function, + plain_serializer_function_ser_schema, + str_schema, + union_schema, +) +from uuid6 import UUID + + +class UUID7Field(UUID): + @classmethod + def __get_pydantic_core_schema__( + cls, + _source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + return json_or_python_schema( + json_schema=str_schema(), + python_schema=union_schema([is_instance_schema(cls), no_info_plain_validator_function(cls.validate)]), + serialization=plain_serializer_function_ser_schema( + lambda x: str(x), + return_schema=str_schema(), + ), + ) + + @classmethod + def validate(cls, v): + if isinstance(v, UUID): + return v + try: + return UUID(str(v)) + except ValueError as e: + raise PydanticCustomError("uuid_parsing", "Invalid UUID format") from e diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..00608a3 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,24 @@ +import asyncio +import os +import sys + +from sqlalchemy.ext.asyncio import create_async_engine + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + +import os +import sys + +from app.core.config import settings +from app.models import Base + +engine = create_async_engine(settings.DATABASE_URL, echo=True) + + +async def create_tables(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +if __name__ == "__main__": + asyncio.run(create_tables()) diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..370ef59 --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,63 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional, Type + +from fastapi import status + + +class APIException(Exception): + status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR + default_message: str = "Internal server error" + + def __init__( + self, message: Optional[str] = None, detail: Optional[Any] = None, headers: Optional[Dict[str, str]] = None + ): + self.message = message or self.default_message + self.detail = detail + self.headers = headers or {} + + super().__init__(self.message) + + +def create_exception(name: str, status_code: int, default_message: str) -> Type[APIException]: + return type(name, (APIException,), {"status_code": status_code, "default_message": default_message}) + + +def prepare_error_response(message: str, detail: Any = None, error_type: Optional[str] = None) -> Dict[str, Any]: + response = {"detail": message} + + if detail is not None: + if isinstance(detail, dict): + response.update(detail) + else: + response["additional_info"] = detail + + if error_type: + response["error_type"] = error_type + + return response + + +BadRequestException = create_exception( + "BadRequestException", status.HTTP_400_BAD_REQUEST, HTTPStatus.BAD_REQUEST.description +) + +NotFoundException = create_exception("NotFoundException", status.HTTP_404_NOT_FOUND, HTTPStatus.NOT_FOUND.description) + +ForbiddenException = create_exception( + "ForbiddenException", status.HTTP_403_FORBIDDEN, HTTPStatus.FORBIDDEN.description +) + +UnauthorizedException = create_exception( + "UnauthorizedException", status.HTTP_401_UNAUTHORIZED, HTTPStatus.UNAUTHORIZED.description +) + +UnprocessableEntity = create_exception( + "UnprocessableEntity", status.HTTP_422_UNPROCESSABLE_ENTITY, HTTPStatus.UNPROCESSABLE_ENTITY.description +) + +DuplicateValueException = create_exception( + "DuplicateValueException", status.HTTP_422_UNPROCESSABLE_ENTITY, HTTPStatus.UNPROCESSABLE_ENTITY.description +) +InvalidInputException = create_exception( + "InvalidInputException", status.HTTP_422_UNPROCESSABLE_ENTITY, HTTPStatus.UNPROCESSABLE_ENTITY.description +) diff --git a/app/core/minio_client.py b/app/core/minio_client.py new file mode 100644 index 0000000..8767d4c --- /dev/null +++ b/app/core/minio_client.py @@ -0,0 +1,193 @@ +from typing import Any, BinaryIO, Dict, List, Optional, Tuple +from urllib.parse import urlparse + +import aiohttp +from fastapi import HTTPException, status +from miniopy_async import Minio +from miniopy_async.error import S3Error + +from app.core.config import settings + + +class MinioClient: + """ + Client class untuk interaksi dengan MinIO Object Storage secara asinkron. + """ + + def __init__(self): + self.client = Minio( + endpoint=settings.MINIO_ENDPOINT_URL, + access_key=settings.MINIO_ROOT_USER, + secret_key=settings.MINIO_ROOT_PASSWORD, + secure=settings.MINIO_SECURE, + region=settings.MINIO_REGION, + ) + self.bucket_name = settings.MINIO_BUCKET_NAME + + async def init_bucket(self) -> None: + """ + Inisialisasi bucket jika belum ada. + """ + try: + if not await self.client.bucket_exists(self.bucket_name): + await self.client.make_bucket(self.bucket_name) + # Set bucket policy agar dapat diakses publik jika diperlukan + # policy = {...} # Define your policy if needed + # await self.client.set_bucket_policy(self.bucket_name, json.dumps(policy)) + except S3Error as err: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error initializing MinIO bucket: {str(err)}", + ) + + async def upload_file( + self, + file_data: BinaryIO, + object_name: str, + content_type: str, + content_length: int, + metadata: Optional[Dict[str, str]] = None, + ) -> str: + """ + Upload file ke MinIO. + + Args: + file_data: File-like object untuk diupload + object_name: Nama objek di MinIO + content_type: Tipe konten file + metadata: Metadata tambahan untuk objek + + Returns: + URL objek yang telah diupload + """ + try: + await self.init_bucket() + + # Upload file + await self.client.put_object( + bucket_name=self.bucket_name, + object_name=object_name, + data=file_data, + length=content_length, + content_type=content_type, + metadata=metadata, + ) + + # Generate URL + url = await self.get_file_url(object_name) + return url + + except S3Error as err: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error uploading file to MinIO: {str(err)}" + ) + + async def get_file(self, object_name: str) -> Tuple[BinaryIO, Dict[str, Any]]: + """ + Ambil file dari MinIO. + + Args: + object_name: Nama objek di MinIO + + Returns: + Tuple dari (file data, object info) + """ + try: + stat = await self.client.stat_object(bucket_name=self.bucket_name, object_name=object_name) + + session = aiohttp.ClientSession() + response = await self.client.get_object( + bucket_name=self.bucket_name, object_name=object_name, session=session + ) + + return response, stat.__dict__ + + except S3Error as err: + if err.code == "NoSuchKey": + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error retrieving file from MinIO: {str(err)}", + ) + + async def delete_file(self, object_name: str) -> bool: + """ + Hapus file dari MinIO. + + Args: + object_name: Nama objek di MinIO + + Returns: + Boolean yang menunjukkan keberhasilan operasi + """ + try: + await self.client.remove_object(self.bucket_name, object_name) + return True + except S3Error as err: + if err.code == "NoSuchKey": + return False + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting file from MinIO: {str(err)}" + ) + + async def get_file_url(self, object_name: str) -> str: + """ + Dapatkan URL untuk mengakses file. + + Args: + object_name: Nama objek di MinIO + + Returns: + URL untuk mengakses file + """ + try: + # For public access + if settings.MINIO_SECURE: + protocol = "https" + else: + protocol = "http" + + parsed_endpoint = urlparse(settings.MINIO_ENDPOINT_URL) + host = parsed_endpoint.netloc or settings.MINIO_ENDPOINT_URL + + return f"{protocol}://{host}/{self.bucket_name}/{object_name}" + + # For presigned URL (time-limited access): + # return await self.client.presigned_get_object( + # bucket_name=self.bucket_name, + # object_name=object_name, + # expires=timedelta(hours=1) + # ) + + except S3Error as err: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error generating URL: {str(err)}" + ) + + async def list_files(self, prefix: str = "", recursive: bool = True) -> List[Dict[str, Any]]: + """ + Daftar semua file di dalam bucket dengan prefix tertentu. + + Args: + prefix: Awalan objek yang dicari + recursive: Jika True, juga mencari di subdirektori + + Returns: + List dari item objek + """ + try: + objects = [] + async for obj in self.client.list_objects(self.bucket_name, prefix=prefix, recursive=recursive): + objects.append( + { + "name": obj.object_name, + "size": obj.size, + "last_modified": obj.last_modified, + "etag": obj.etag, + } + ) + return objects + except S3Error as err: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error listing files: {str(err)}" + ) diff --git a/app/core/params.py b/app/core/params.py new file mode 100644 index 0000000..db28e23 --- /dev/null +++ b/app/core/params.py @@ -0,0 +1,36 @@ +import json +from typing import Optional + +from fastapi import Query + + +class CommonParams: + def __init__( + self, + filter: Optional[str] = Query(default=None), + sort: Optional[str] = Query(default=None), + search: str = Query(default=""), + group_by: Optional[str] = Query(default=None), + limit: int = Query(default=100, ge=1), + offset: int = Query(default=0, ge=0), + ): + if filter: + try: + self.filter = json.loads(filter) + except Exception: + self.filter = filter + else: + self.filter = [] + + if sort: + try: + self.sort = json.loads(sort) + except Exception: + self.sort = sort + else: + self.sort = [] + + self.search = search + self.group_by = group_by + self.limit = limit + self.offset = offset diff --git a/app/core/responses.py b/app/core/responses.py new file mode 100644 index 0000000..98e9a5a --- /dev/null +++ b/app/core/responses.py @@ -0,0 +1,16 @@ +from typing import Any, override + +from fastapi.responses import JSONResponse + +from app.utils.helpers import orjson_dumps + + +class ORJSONResponse(JSONResponse): + """Custom JSONResponse menggunakan orjson.""" + + media_type = "application/json" + + @override + def render(self, content: Any) -> bytes: + """Render content menggunakan orjson.""" + return orjson_dumps(content) diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..9f6bf8e --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,80 @@ +# app/core/security.py +from datetime import datetime, timedelta +from typing import Any, Dict, Optional, Union + +from fastapi import HTTPException +from jose import ExpiredSignatureError, JWTError, jwt +from passlib.context import CryptContext +from pytz import timezone + +from app.core.config import settings + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verifikasi password.""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Hash password.""" + return pwd_context.hash(password) + + +def create_token( + subject: Union[str, Any], expires_delta: Optional[timedelta] = None, token_type: str = "access" +) -> str: + """Buat JWT token.""" + if expires_delta: + expire = datetime.now(timezone(settings.TIMEZONE)) + expires_delta + else: + if token_type == "access": + expire = datetime.now(timezone(settings.TIMEZONE)) + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + elif token_type == "refresh": + expire = datetime.now(timezone(settings.TIMEZONE)) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + else: + expire = datetime.now(timezone(settings.TIMEZONE)) + timedelta(minutes=15) + + to_encode = { + "exp": expire, + "iat": datetime.now(timezone(settings.TIMEZONE)), + "sub": str(subject), + "type": token_type, + } + + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + + return encoded_jwt + + +def create_access_token(subject: Union[str, Any]) -> str: + """Buat access token.""" + return create_token(subject, token_type="access") + + +def create_refresh_token(subject: Union[str, Any]) -> str: + """Buat refresh token.""" + return create_token(subject, token_type="refresh") + + +def decode_token(token: str) -> Dict[str, Any]: + try: + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=[settings.ALGORITHM], + options={ + "verify_signature": True, + "verify_exp": True, + "verify_iat": True, + }, + ) + return payload + except ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token expired") + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..ce0a1a9 --- /dev/null +++ b/app/main.py @@ -0,0 +1,117 @@ +# local run +# pyenv local 3.13.0 +# poetry env use python +# poetry install +# poetry install --no-root + +# poetry run uvicorn app.main:app --reload + + +from contextlib import asynccontextmanager + +from asyncpg.exceptions import ForeignKeyViolationError +from brotli_asgi import BrotliMiddleware +from fastapi import FastAPI, Request, status +from fastapi.exceptions import HTTPException, RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, ORJSONResponse +from fastapi_async_sqlalchemy import SQLAlchemyMiddleware +from sqlalchemy.exc import IntegrityError + +from app.api.v1 import router as api_router +from app.core.config import settings +from app.core.exceptions import APIException, prepare_error_response +from app.utils.system import optimize_system + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await optimize_system() + yield + + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, + default_response_class=ORJSONResponse, + lifespan=lifespan, + root_path="/api", + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", +) + +app.add_middleware( + BrotliMiddleware, + minimum_size=1000, +) +app.add_middleware( + SQLAlchemyMiddleware, + db_url=settings.DATABASE_URL, + engine_args={"echo": settings.DEBUG}, +) +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router) + + +@app.exception_handler(APIException) +async def api_exception_handler(request: Request, exc: APIException): + response_content = prepare_error_response(message=exc.message) + + return JSONResponse(status_code=exc.status_code, content=response_content, headers=exc.headers) + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + response_content = prepare_error_response(message=str(exc.detail)) + + return JSONResponse(status_code=exc.status_code, content=response_content, headers=exc.headers) + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + response_content = prepare_error_response(message=exc.errors()) + + return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=response_content) + + +@app.exception_handler(LookupError) +async def enum_exception_handler(request: Request, exc: LookupError): + error_message = str(exc) + + if "is not among the defined enum values" in error_message: + response_content = prepare_error_response( + message="Invalid enum value", + detail=error_message, + ) + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=response_content) + + raise exc + + +@app.exception_handler(ForeignKeyViolationError) +@app.exception_handler(IntegrityError) +async def global_exception_handler(request: Request, exc: Exception): + response_content = prepare_error_response( + message="Foreign key violation", + detail=str(exc) if settings.DEBUG else None, + ) + + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=response_content) + + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + response_content = prepare_error_response( + message="Internal server error", detail=str(exc) if settings.DEBUG else None + ) + + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=response_content) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..9d8db49 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,38 @@ +from .base import Base +from .category_model import CategoryModel +from .classification_model import ClassificationModel +from .credential_model import CredentialModel +from .file_model import FileModel +from .map_access_model import MapAccessModel +from .map_projection_system_model import MapProjectionSystemModel +from .map_source_model import MapSourceModel, SourceUsageModel +from .mapset_history_model import MapsetHistoryModel +from .mapset_model import MapsetModel +from .news_model import NewsModel +from .organization_model import OrganizationModel +from .refresh_token_model import RefreshTokenModel +from .regional_model import RegionalModel +from .role_model import RoleModel +from .user_model import UserModel +from .feedback_model import FeedbackModel + +__all__ = [ + "Base", + "OrganizationModel", + "RoleModel", + "UserModel", + "RefreshTokenModel", + "NewsModel", + "FileModel", + "CredentialModel", + "MapsetModel", + "MapSourceModel", + "MapProjectionSystemModel", + "MapAccessModel", + "MapsetHistoryModel", + "CategoryModel", + "ClassificationModel", + "RegionalModel", + "SourceUsageModel", + "FeedbackModel", +] diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..4246f0d --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,12 @@ +from typing import Any, Dict + +from sqlalchemy.orm import declarative_base + +from app.utils.helpers import orm_to_dict + + +class Base(declarative_base()): + __abstract__ = True + + def to_dict(self) -> Dict[str, Any]: + return orm_to_dict(self) diff --git a/app/models/category_model.py b/app/models/category_model.py new file mode 100644 index 0000000..d116b07 --- /dev/null +++ b/app/models/category_model.py @@ -0,0 +1,16 @@ +import uuid6 +from sqlalchemy import UUID, Boolean, Column, Integer, String, Text, text + +from . import Base + + +class CategoryModel(Base): + __tablename__ = "categories" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String) + description = Column(Text) + thumbnail = Column(String) + count_mapset = Column(Integer, default=0, server_default=text("0")) + is_active = Column(Boolean, default=True) + order = Column(Integer, default=0) diff --git a/app/models/classification_model.py b/app/models/classification_model.py new file mode 100644 index 0000000..7f3e745 --- /dev/null +++ b/app/models/classification_model.py @@ -0,0 +1,14 @@ +import uuid6 +from sqlalchemy import UUID, Boolean, Column, String + +from . import Base + + +class ClassificationModel(Base): + __tablename__ = "classifications" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String(20), nullable=False) + is_open = Column(Boolean, default=True) + is_limited = Column(Boolean, default=False) + is_secret = Column(Boolean, default=False) diff --git a/app/models/credential_model.py b/app/models/credential_model.py new file mode 100644 index 0000000..9c71907 --- /dev/null +++ b/app/models/credential_model.py @@ -0,0 +1,28 @@ +from datetime import datetime + +import uuid6 +from sqlalchemy import JSON, UUID, Boolean, Column, DateTime, ForeignKey, String, Text +from sqlalchemy.orm import Mapped + +from . import Base + + +class CredentialModel(Base): + __tablename__ = "credentials" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String) + description = Column(Text) + encrypted_data = Column(Text, nullable=False) + encryption_iv = Column(String(255), nullable=False) + credential_type = Column(String(50), nullable=False) + credential_metadata: Mapped[dict] = Column(JSON, nullable=True) + created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + updated_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime(timezone=True), default=datetime.now) + updated_at = Column(DateTime(timezone=True), onupdate=datetime.now) + last_used_at = Column(DateTime(timezone=True), nullable=True) + last_used_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + is_default = Column(Boolean, default=False) + is_active = Column(Boolean, default=True) + is_deleted = Column(Boolean, default=False) diff --git a/app/models/feedback_model.py b/app/models/feedback_model.py new file mode 100644 index 0000000..3cf6fc4 --- /dev/null +++ b/app/models/feedback_model.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text, func + +from . import Base + + +class FeedbackModel(Base): + __tablename__ = "feedback" + + id = Column(Integer, primary_key=True, autoincrement=True) + datetime = Column(DateTime, default=datetime.now) + score = Column(Integer) + tujuan_tercapai = Column(Boolean, default=True) + tujuan_ditemukan = Column(Boolean, default=True) + tujuan = Column(String) + sektor = Column(String) + email = Column(String) + saran = Column(Text) + source_url = Column(String) + source_access = Column(String) + notes = Column(Text) + gender = Column(Integer) diff --git a/app/models/file_model.py b/app/models/file_model.py new file mode 100644 index 0000000..5bd001b --- /dev/null +++ b/app/models/file_model.py @@ -0,0 +1,31 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class FileModel(Base): + __tablename__ = "files" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid6.uuid7, index=True) + filename = Column(String(255), nullable=False, index=True) + object_name = Column(String(512), nullable=False, unique=True) + content_type = Column(String(100), nullable=False) + size = Column(Integer, nullable=False) + description = Column(Text, nullable=True) + url = Column(String(1024), nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + modified_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + + uploaded_by = relationship("UserModel", lazy="selectin", uselist=False) diff --git a/app/models/map_access_model.py b/app/models/map_access_model.py new file mode 100644 index 0000000..1ca8656 --- /dev/null +++ b/app/models/map_access_model.py @@ -0,0 +1,45 @@ +from datetime import datetime +from typing import Optional + +from pytz import timezone +from sqlalchemy import UUID as SQLUUID +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String +from sqlalchemy.orm import Mapped +from uuid6 import UUID, uuid7 + +from app.core.config import settings + +from . import Base + + +class MapAccessModel(Base): + __tablename__ = "mapset_access" + + id: Mapped[UUID] = Column(String, primary_key=True, default=uuid7) + mapset_id: Mapped[UUID] = Column( + SQLUUID(as_uuid=True), ForeignKey("mapsets.id", ondelete="CASCADE"), nullable=False + ) + user_id: Mapped[Optional[UUID]] = Column( + SQLUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=True + ) + organization_id: Mapped[Optional[UUID]] = Column( + SQLUUID(as_uuid=True), ForeignKey("organizations.id", ondelete="CASCADE"), nullable=True + ) + granted_by: Mapped[UUID] = Column(SQLUUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + can_read: Mapped[bool] = Column(Boolean, default=True) + can_write: Mapped[bool] = Column(Boolean, default=False) + can_delete: Mapped[bool] = Column(Boolean, default=False) + + created_at: Mapped[datetime] = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + updated_at: Mapped[datetime] = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + # expires_at: Mapped[Optional[datetime]] = Column(DateTime(timezone=True), nullable=True) # Optional expiry + + # Relationships + # mapset = relationship("MapsetModel", back_populates="access_grants") + # user = relationship("UserModel", foreign_keys=[user_id], back_populates="mapset_access") + # organization = relationship("OrganizationModel", back_populates="mapset_access") + # grantor = relationship("UserModel", foreign_keys=[granted_by]) diff --git a/app/models/map_projection_system_model.py b/app/models/map_projection_system_model.py new file mode 100644 index 0000000..12e8ef5 --- /dev/null +++ b/app/models/map_projection_system_model.py @@ -0,0 +1,11 @@ +import uuid6 +from sqlalchemy import UUID, Column, String + +from . import Base + + +class MapProjectionSystemModel(Base): + __tablename__ = "map_projection_systems" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String(50), nullable=False) diff --git a/app/models/map_source_model.py b/app/models/map_source_model.py new file mode 100644 index 0000000..8058009 --- /dev/null +++ b/app/models/map_source_model.py @@ -0,0 +1,56 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, ForeignKey, String, Text +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class MapSourceModel(Base): + __tablename__ = "map_sources" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String(50), nullable=False) + description = Column(Text) + url = Column(Text, nullable=True) + credential_id = Column(UUID(as_uuid=True), ForeignKey("credentials.id")) + is_active = Column(Boolean, default=True) + is_deleted = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + updated_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + + usages = relationship("SourceUsageModel", back_populates="source", lazy="selectin") + mapsets = relationship( + "MapsetModel", + secondary="source_usages", + primaryjoin="MapSourceModel.id == SourceUsageModel.source_id", + secondaryjoin="SourceUsageModel.mapset_id == MapsetModel.id", + lazy="selectin", + viewonly=True, + ) + credential = relationship("CredentialModel", lazy="selectin", uselist=False) + + +class SourceUsageModel(Base): + __tablename__ = "source_usages" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + source_id = Column(UUID(as_uuid=True), ForeignKey("map_sources.id"), nullable=False) + mapset_id = Column(UUID(as_uuid=True), ForeignKey("mapsets.id"), nullable=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + updated_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + + mapset = relationship("MapsetModel", back_populates="source_usages") + source = relationship("MapSourceModel", back_populates="usages") diff --git a/app/models/mapset_history_model.py b/app/models/mapset_history_model.py new file mode 100644 index 0000000..81438d4 --- /dev/null +++ b/app/models/mapset_history_model.py @@ -0,0 +1,27 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Column, DateTime, ForeignKey, String, Text +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class MapsetHistoryModel(Base): + """Model untuk melacak riwayat perubahan pada mapset.""" + + __tablename__ = "mapset_histories" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + mapset_id = Column(UUID(as_uuid=True), ForeignKey("mapsets.id"), index=True, comment="ID mapset yang dilacak") + validation_type = Column(String(50), nullable=False, comment="Jenis perubahan pada mapset") + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), comment="ID pengguna yang melakukan perubahan") + notes = Column(Text, nullable=True, comment="Catatan detail perubahan yang dilakukan") + timestamp = Column( + DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE)), comment="Waktu perubahan tercatat" + ) + + user = relationship("UserModel", uselist=False, lazy="selectin") diff --git a/app/models/mapset_model.py b/app/models/mapset_model.py new file mode 100644 index 0000000..1dd4ba7 --- /dev/null +++ b/app/models/mapset_model.py @@ -0,0 +1,69 @@ +from datetime import datetime +from enum import Enum + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class MapsetStatus(str, Enum): + approved = "approved" + rejected = "rejected" + on_verification = "on_verification" + + +class MapsetModel(Base): + __tablename__ = "mapsets" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + scale = Column(String(29), nullable=False) + layer_url = Column(Text) + layer_type = Column(String(20), nullable=True) + metadata_url = Column(Text) + category_id = Column(UUID(as_uuid=True), ForeignKey("categories.id")) + classification_id = Column(UUID(as_uuid=True), ForeignKey("classifications.id")) + regional_id = Column(UUID(as_uuid=True), ForeignKey("regionals.id"), nullable=True) + projection_system_id = Column(UUID(as_uuid=True), ForeignKey("map_projection_systems.id")) + producer_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id")) + status_validation = Column(String(20), nullable=True) + data_status = Column(String(20), nullable=False) + data_update_period = Column(String(20), nullable=False) + data_version = Column(String(20), nullable=False) + coverage_level = Column(String(20), nullable=True) + coverage_area = Column(String(20), nullable=True) + view_count = Column(Integer, default=0) + download_count = Column(Integer, default=0) + order = Column(Integer, default=0) + is_popular = Column(Boolean, default=False) + is_active = Column(Boolean, default=True) + is_deleted = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + updated_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) + updated_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) + + projection_system = relationship("MapProjectionSystemModel", uselist=False, lazy="selectin") + classification = relationship("ClassificationModel", uselist=False, lazy="selectin") + category = relationship("CategoryModel", uselist=False, lazy="selectin") + regional = relationship("RegionalModel", uselist=False, lazy="selectin") + source_usages = relationship("SourceUsageModel", back_populates="mapset", lazy="selectin") + sources = relationship( + "MapSourceModel", + secondary="source_usages", + primaryjoin="MapsetModel.id == SourceUsageModel.mapset_id", + secondaryjoin="SourceUsageModel.source_id == MapSourceModel.id", + lazy="selectin", + viewonly=True, + ) + producer = relationship("OrganizationModel", back_populates="mapsets", uselist=False, lazy="joined") diff --git a/app/models/news_model.py b/app/models/news_model.py new file mode 100644 index 0000000..6304584 --- /dev/null +++ b/app/models/news_model.py @@ -0,0 +1,29 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, String, Text, func + +from app.core.config import settings + +from . import Base + + +class NewsModel(Base): + __tablename__ = "news" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String) + description = Column(Text) + thumbnail = Column(String) + created_at = Column( + DateTime(timezone=True), server_default=func.now(), default=datetime.now(timezone(settings.TIMEZONE)) + ) + updated_at = Column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + default=datetime.now(timezone(settings.TIMEZONE)), + ) + is_active = Column(Boolean, default=True) + is_deleted = Column(Boolean, default=False) diff --git a/app/models/organization_model.py b/app/models/organization_model.py new file mode 100644 index 0000000..bd20414 --- /dev/null +++ b/app/models/organization_model.py @@ -0,0 +1,42 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, String, Text +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class OrganizationModel(Base): + __tablename__ = "organizations" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String(100), nullable=False) + description = Column(Text, nullable=True) + thumbnail = Column(String(255), nullable=True) + address = Column(String(255), nullable=True) + phone_number = Column(String(15), nullable=True) + email = Column(String(100), nullable=True) + website = Column(String(255), nullable=True) + is_active = Column(Boolean, default=True, server_default="true") + is_deleted = Column(Boolean, default=False, server_default="false") + created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.now(timezone(settings.TIMEZONE))) + modified_at = Column( + DateTime(timezone=True), + nullable=True, + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + + users = relationship("UserModel", lazy="selectin") + mapsets = relationship("MapsetModel", lazy="selectin") + + # @property + # def count_mapset(self): + # if self.mapsets is None: + # return 0 + # else: + # return len(self.mapsets) diff --git a/app/models/refresh_token_model.py b/app/models/refresh_token_model.py new file mode 100644 index 0000000..310ed2d --- /dev/null +++ b/app/models/refresh_token_model.py @@ -0,0 +1,23 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, ForeignKey, String +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class RefreshTokenModel(Base): + __tablename__ = "refresh_tokens" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) + token = Column(String(255), nullable=False, index=True) + expires_at = Column(DateTime(timezone=True), nullable=False) + revoked = Column(Boolean, default=False, server_default="false") + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + + user = relationship("UserModel", lazy="selectin", uselist=False) diff --git a/app/models/regional_model.py b/app/models/regional_model.py new file mode 100644 index 0000000..832a3fa --- /dev/null +++ b/app/models/regional_model.py @@ -0,0 +1,26 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, String, Text + +from app.core.config import settings + +from . import Base + + +class RegionalModel(Base): + __tablename__ = "regionals" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + code = Column(String(10), nullable=False) + name = Column(String(50), nullable=False) + description = Column(Text, nullable=True) + thumbnail = Column(String(255), nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + updated_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) diff --git a/app/models/role_model.py b/app/models/role_model.py new file mode 100644 index 0000000..56f16f7 --- /dev/null +++ b/app/models/role_model.py @@ -0,0 +1,32 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, String, Text +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class RoleModel(Base): + __tablename__ = "roles" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String(20), nullable=False) + description = Column(Text, nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + updated_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + + users = relationship("UserModel", lazy="selectin") + + # Relationships + # organization = relationship("OrganizationModel", back_populates="members") + # produced_mapsets = relationship("MapsetModel", back_populates="producer") + # mapset_access = relationship("MapsetAccessModel", foreign_keys=[MapsetAccessModel.user_id], back_populates="user") diff --git a/app/models/user_model.py b/app/models/user_model.py new file mode 100644 index 0000000..7ee77b7 --- /dev/null +++ b/app/models/user_model.py @@ -0,0 +1,36 @@ +from datetime import datetime + +import uuid6 +from pytz import timezone +from sqlalchemy import UUID, Boolean, Column, DateTime, ForeignKey, String +from sqlalchemy.orm import relationship + +from app.core.config import settings + +from . import Base + + +class UserModel(Base): + __tablename__ = "users" + + id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid6.uuid7) + name = Column(String, nullable=False) + email = Column(String, unique=True, nullable=False) + profile_picture = Column(String, nullable=True) + username = Column(String, unique=True, nullable=False) + password = Column(String, nullable=False) + position = Column(String, nullable=True) + role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"), nullable=False) + employee_id = Column(String, nullable=True) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False) + is_active = Column(Boolean, default=True) + is_deleted = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), default=datetime.now(timezone(settings.TIMEZONE))) + modified_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone(settings.TIMEZONE)), + onupdate=datetime.now(timezone(settings.TIMEZONE)), + ) + + organization = relationship("OrganizationModel", back_populates="users", lazy="selectin", uselist=False) + role = relationship("RoleModel", back_populates="users", lazy="selectin", uselist=False) diff --git a/app/repositories/__init__.py b/app/repositories/__init__.py new file mode 100644 index 0000000..bf6affa --- /dev/null +++ b/app/repositories/__init__.py @@ -0,0 +1,39 @@ +from .base import BaseRepository +from .category_repository import CategoryRepository +from .classification_repository import ClassificationRepository +from .credential_repository import CredentialRepository +from .file_repository import FileRepository +from .map_access_repository import MapAccessRepository +from .map_projection_system_repository import MapProjectionSystemRepository +from .map_source_repository import MapSourceRepository +from .map_source_usage_repository import SourceUsageRepository +from .mapset_history_repository import MapsetHistoryRepository +from .mapset_repository import MapsetRepository +from .news_repository import NewsRepository +from .organization_repository import OrganizationRepository +from .regional_repository import RegionalRepository +from .role_repository import RoleRepository +from .token_repository import TokenRepository +from .user_repository import UserRepository +from .feedback_repository import FeedbackRepository + +__all__ = [ + "BaseRepository", + "OrganizationRepository", + "RoleRepository", + "UserRepository", + "TokenRepository", + "NewsRepository", + "FileRepository", + "CredentialRepository", + "MapSourceRepository", + "MapProjectionSystemRepository", + "MapAccessRepository", + "CategoryRepository", + "ClassificationRepository", + "FeedbackRepository", + "RegionalRepository", + "MapsetRepository", + "MapsetHistoryRepository", + "SourceUsageRepository", +] diff --git a/app/repositories/base.py b/app/repositories/base.py new file mode 100644 index 0000000..3259ed1 --- /dev/null +++ b/app/repositories/base.py @@ -0,0 +1,178 @@ +from typing import Any, Dict, Generic, List, Optional, Tuple, Type, TypeVar + +from fastapi_async_sqlalchemy import db +from sqlalchemy import String, cast +from sqlalchemy import delete as sqlalchemy_delete +from sqlalchemy import func, or_, select +from sqlalchemy import update as sqlalchemy_update +from sqlalchemy.orm import joinedload, selectinload +from uuid6 import UUID + +from app.core.database import Base + +ModelType = TypeVar("ModelType", bound=Base) + + +class BaseRepository(Generic[ModelType]): + """Optimized base repository with fastapi-async-sqlalchemy.""" + + def __init__(self, model: Type[ModelType]): + self.model: Type[ModelType] = model + + def build_base_query(self, include_deleted: bool = False): + """Build base query dengan soft delete handling.""" + query = select(self.model) + if hasattr(self.model, "is_deleted") and not include_deleted: + query = query.where(self.model.is_deleted.is_(False)) + return query + + async def find_by_id(self, id: UUID, relationships: List[str] = None) -> Optional[ModelType]: + """Find record by ID dengan optional eager loading.""" + query = self.build_base_query().where(self.model.id == id) + + if relationships: + for rel in relationships: + if hasattr(self.model, rel): + attr = getattr(self.model, rel) + if hasattr(attr.property, "collection_class"): + query = query.options(selectinload(attr)) + else: + query = query.options(joinedload(attr)) + + result = await db.session.execute(query) + return result.scalar_one_or_none() + + async def find_all( + self, + filters: list = [], + sort: list = [], + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + relationships: List[str] = None, + searchable_columns: List[str] = None, + ) -> Tuple[List[ModelType], int]: + """Optimized find_all method.""" + + query = self.build_base_query().filter(*filters) + + # Optimized search + if search: + if searchable_columns: + search_conditions = [ + cast(getattr(self.model, col), String).ilike(f"%{search}%") + for col in searchable_columns + if hasattr(self.model, col) + ] + else: + search_conditions = [ + cast(getattr(self.model, col), String).ilike(f"%{search}%") + for col in self.model.__table__.columns.keys() + if not col.startswith("_") + ] + + if search_conditions: + query = query.where(or_(*search_conditions)) + + if group_by: + query = query.group_by(getattr(self.model, group_by)) + + # Count query + count_query = select(func.count()).select_from(query.subquery()) + total = await db.session.scalar(count_query) + + # Data query + if sort: + query = query.order_by(*sort) + else: + query = query.order_by(self.model.id) + + if relationships: + for rel in relationships: + if hasattr(self.model, rel): + attr = getattr(self.model, rel) + if hasattr(attr.property, "collection_class"): + query = query.options(selectinload(attr)) + else: + query = query.options(joinedload(attr)) + + query = query.limit(limit).offset(offset) + result = await db.session.execute(query) + records = result.scalars().all() + + return records, total + + async def create(self, data: Dict[str, Any]) -> ModelType: + """Create new record.""" + new_record = self.model(**data) + db.session.add(new_record) + await db.session.commit() + await db.session.refresh(new_record) + return new_record + + async def bulk_create( + self, data: List[Dict[str, Any]], batch_size: int = 1000, return_records: bool = False + ) -> Optional[List[ModelType]]: + """Bulk create dengan batching.""" + if not data: + return [] if return_records else None + + created_records = [] + + for i in range(0, len(data), batch_size): + batch = data[i : i + batch_size] + + if return_records: + batch_records = [self.model(**item) for item in batch] + db.session.add_all(batch_records) + created_records.extend(batch_records) + else: + await db.session.execute(self.model.__table__.insert(), batch) + + await db.session.commit() + + if return_records: + for record in created_records: + await db.session.refresh(record) + return created_records + + return None + + async def update(self, id: UUID, data: Dict[str, Any], refresh: bool = True) -> Optional[ModelType]: + """Update record dengan optimization.""" + clean_data = {k: v for k, v in data.items() if v is not None} + + if not clean_data: + return await self.find_by_id(id) if refresh else None + + query = ( + sqlalchemy_update(self.model) + .where(self.model.id == id) + .values(**clean_data) + .execution_options(synchronize_session="fetch") + ) + + result = await db.session.execute(query) + await db.session.commit() + + if result.rowcount == 0: + return None + + return await self.find_by_id(id) if refresh else None + + async def delete(self, id: UUID) -> bool: + """Delete record.""" + query = sqlalchemy_delete(self.model).where(self.model.id == id) + result = await db.session.execute(query) + await db.session.commit() + return result.rowcount > 0 + + async def exists(self, id: UUID) -> bool: + """Check if record exists.""" + query = select(1).where(self.model.id == id) + if hasattr(self.model, "is_deleted"): + query = query.where(self.model.is_deleted.is_(False)) + + result = await db.session.scalar(query) + return result is not None diff --git a/app/repositories/category_repository.py b/app/repositories/category_repository.py new file mode 100644 index 0000000..0da35c1 --- /dev/null +++ b/app/repositories/category_repository.py @@ -0,0 +1,143 @@ +from app.models import CategoryModel, MapsetModel, ClassificationModel +from sqlalchemy import func, or_, cast, String, select +from sqlalchemy.orm import joinedload, selectinload +from fastapi_async_sqlalchemy import db +from typing import List, Tuple, Optional +from uuid import UUID + +from . import BaseRepository + + +class CategoryRepository(BaseRepository[CategoryModel]): + def __init__(self, model): + super().__init__(model) + + async def find_by_id(self, id: UUID, relationships: List[str] = None) -> Optional[CategoryModel]: + """Find category by ID with mapset count.""" + # Create subquery for mapset count + mapset_count_subquery = ( + select(func.count(MapsetModel.id)) + .join(ClassificationModel, MapsetModel.classification_id == ClassificationModel.id) + .where(MapsetModel.category_id == id) + .where(MapsetModel.is_deleted == False) + .where(MapsetModel.is_active == True) + .where(MapsetModel.status_validation == "approved") + .where(ClassificationModel.is_open == True) + .scalar_subquery() + ) + + # Build query with mapset count + query = ( + select(self.model, mapset_count_subquery.label('mapset_count')) + .where(self.model.id == id) + ) + + if hasattr(self.model, "is_deleted"): + query = query.where(self.model.is_deleted.is_(False)) + + if relationships: + for rel in relationships: + if hasattr(self.model, rel): + attr = getattr(self.model, rel) + if hasattr(attr.property, "collection_class"): + query = query.options(selectinload(attr)) + else: + query = query.options(joinedload(attr)) + + result = await db.session.execute(query) + row = result.first() + + if row: + category = row[0] + category.count_mapset = row[1] if row[1] is not None else 0 + return category + + return None + + async def find_all( + self, + filters: list = [], + sort: list = [], + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + relationships: List[str] = None, + searchable_columns: List[str] = None, + ) -> Tuple[List[CategoryModel], int]: + """Optimized find_all method with mapset count.""" + + # Create subquery for mapset count + mapset_count_subquery = ( + select( + MapsetModel.category_id, + func.count(MapsetModel.id).label('mapset_count') + ) + .join(ClassificationModel, MapsetModel.classification_id == ClassificationModel.id) + .where(MapsetModel.is_deleted == False) + .where(MapsetModel.is_active == True) + .where(MapsetModel.status_validation == "approved") + .where(ClassificationModel.is_open == True) + .group_by(MapsetModel.category_id) + .subquery() + ) + + # Build base query with mapset count + query = ( + select(self.model, func.coalesce(mapset_count_subquery.c.mapset_count, 0).label('mapset_count')) + .outerjoin(mapset_count_subquery, self.model.id == mapset_count_subquery.c.category_id) + .filter(*filters) + ) + + # Optimized search + if search: + if searchable_columns: + search_conditions = [ + cast(getattr(self.model, col), String).ilike(f"%{search}%") + for col in searchable_columns + if hasattr(self.model, col) + ] + else: + search_conditions = [ + cast(getattr(self.model, col), String).ilike(f"%{search}%") + for col in self.model.__table__.columns.keys() + if not col.startswith("_") + ] + + if search_conditions: + query = query.where(or_(*search_conditions)) + + if group_by: + query = query.group_by(getattr(self.model, group_by)) + + # Count query + count_query = select(func.count()).select_from(query.subquery()) + total = await db.session.scalar(count_query) + + # Data query + if sort: + query = query.order_by(*sort) + else: + query = query.order_by(self.model.order.asc()) + + if relationships: + for rel in relationships: + if hasattr(self.model, rel): + attr = getattr(self.model, rel) + if hasattr(attr.property, "collection_class"): + query = query.options(selectinload(attr)) + else: + query = query.options(joinedload(attr)) + + query = query.limit(limit).offset(offset) + result = await db.session.execute(query) + + # Extract records and set mapset_count + records = [] + for row in result: + category = row[0] + category.count_mapset = row[1] + records.append(category) + + return records, total + diff --git a/app/repositories/classification_repository.py b/app/repositories/classification_repository.py new file mode 100644 index 0000000..e42a569 --- /dev/null +++ b/app/repositories/classification_repository.py @@ -0,0 +1,8 @@ +from app.models import ClassificationModel + +from . import BaseRepository + + +class ClassificationRepository(BaseRepository[ClassificationModel]): + def __init__(self, model): + super().__init__(model) diff --git a/app/repositories/credential_repository.py b/app/repositories/credential_repository.py new file mode 100644 index 0000000..7bd464c --- /dev/null +++ b/app/repositories/credential_repository.py @@ -0,0 +1,120 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from fastapi_async_sqlalchemy import db +from pytz import timezone +from sqlalchemy import select, update +from uuid6 import UUID + +from app.core.config import settings +from app.models import CredentialModel + +from . import BaseRepository + + +class CredentialRepository(BaseRepository[CredentialModel]): + def __init__(self, model): + super().__init__(model) + + async def create(self, data: Dict[str, Any]): + credential = await super().create(data) + if credential.is_default: + await self.set_default(credential.id, credential.created_by) + + return credential + + async def get_by_type(self, credential_type: str, is_active: bool = True) -> List[CredentialModel]: + """ + Mendapatkan semua kredensial berdasarkan tipe. + + Args: + credential_type: Tipe kredensial ('database', 'api', 'minio', dll) + is_active: Filter berdasarkan status aktif + + Returns: + List dari credential models + """ + query = select(self.model).where(self.model.credential_type == credential_type) + + if is_active is not None: + query = query.where(self.model.is_active == is_active) + + query = query.order_by(self.model.created_at.desc()) + result = await db.session.execute(query) + return result.scalars().all() + + async def get_default_by_type(self, credential_type: str, is_active: bool = True) -> Optional[CredentialModel]: + """ + Mendapatkan kredensial default berdasarkan tipe. + + Args: + credential_type: Tipe kredensial ('database', 'api', 'minio', dll) + is_active: Filter berdasarkan status aktif + + Returns: + Credential model atau None jika tidak ditemukan + """ + query = select(self.model).where(self.model.credential_type == credential_type, self.model.is_default == True) + + if is_active is not None: + query = query.where(self.model.is_active == is_active) + + result = await db.session.execute(query) + return result.scalars().first() + + async def set_default(self, credential_id: UUID, updated_by: UUID) -> bool: + """ + Set kredensial sebagai default untuk tipenya, dan unset default + untuk kredensial lain dengan tipe yang sama. + + Args: + credential_id: ID kredensial yang akan dijadikan default + updated_by: ID user yang melakukan update + + Returns: + Boolean yang menunjukkan keberhasilan operasi + """ + # Dapatkan kredensial yang akan diset sebagai default + cred = await self.find_by_id(credential_id) + if not cred: + return False + + # Reset default flag untuk semua kredensial dengan tipe yang sama + reset_query = ( + update(self.model) + .where(self.model.credential_type == cred.credential_type) + .values(is_default=False, updated_by=updated_by) + ) + await db.session.execute(reset_query) + + # Set sebagai default + set_query = ( + update(self.model).where(self.model.id == credential_id).values(is_default=True, updated_by=updated_by) + ) + await db.session.execute(set_query) + await db.session.commit() + + return True + + async def update_last_used(self, credential_id: UUID, user_id: UUID) -> bool: + """ + Update timestamp penggunaan terakhir. + + Args: + db: Database session + credential_id: ID kredensial yang digunakan + user_id: ID user yang menggunakan + + Returns: + Boolean yang menunjukkan keberhasilan operasi + """ + update_query = ( + update(self.model) + .where(self.model.id == credential_id) + .values(last_used_at=datetime.now(timezone(settings.TIMEZONE)), last_used_by=user_id) + ) + + await db.session.execute(update_query) + await db.session.commit() + + return True diff --git a/app/repositories/feedback_repository.py b/app/repositories/feedback_repository.py new file mode 100644 index 0000000..d69fa9e --- /dev/null +++ b/app/repositories/feedback_repository.py @@ -0,0 +1,6 @@ +from app.models.feedback_model import FeedbackModel +from app.repositories.base import BaseRepository + + +class FeedbackRepository(BaseRepository): + model = FeedbackModel diff --git a/app/repositories/file_repository.py b/app/repositories/file_repository.py new file mode 100644 index 0000000..c78398c --- /dev/null +++ b/app/repositories/file_repository.py @@ -0,0 +1,16 @@ +from fastapi_async_sqlalchemy import db +from sqlalchemy import select + +from app.models import FileModel + +from . import BaseRepository + + +class FileRepository(BaseRepository[FileModel]): + def __init__(self, model): + super().__init__(model) + + async def find_by_user_id(self, user_id: int): + query = select(self.model.user_id).where(self.model.user_id == user_id) + result = await db.session.execute(query) + return result.scalars().all() diff --git a/app/repositories/map_access_repository.py b/app/repositories/map_access_repository.py new file mode 100644 index 0000000..d3bde21 --- /dev/null +++ b/app/repositories/map_access_repository.py @@ -0,0 +1,39 @@ +from fastapi_async_sqlalchemy import db +from sqlalchemy import select +from uuid6 import UUID + +from app.models import MapAccessModel + +from . import BaseRepository + + +class MapAccessRepository(BaseRepository[MapAccessModel]): + def __init__(self, model): + super().__init__(model) + + async def find_by_mapset(self, mapset_id: UUID): + query = select(self.model).where(self.model.mapset_id == mapset_id) + result = await db.session.execute(query) + return result.scalars().all() + + async def find_by_user(self, user_id: UUID): + query = select(self.model).where(self.model.user_id == user_id) + result = await db.session.execute(query) + return result.scalars().all() + + async def find_by_organization(self, organization_id: UUID): + query = select(self.model).where(self.model.organization_id == organization_id) + result = await db.session.execute(query) + return result.scalars().all() + + async def find_user_access_to_mapset(self, mapset_id: UUID, user_id: UUID): + query = select(self.model).where(self.model.mapset_id == mapset_id, self.model.user_id == user_id) + result = await db.session.execute(query) + return result.scalars().all() + + async def find_organization_access_to_mapset(self, mapset_id: UUID, organization_id: UUID): + query = select(self.model).where( + self.model.mapset_id == mapset_id, self.model.organization_id == organization_id + ) + result = await db.session.execute(query) + return result.scalars().all() diff --git a/app/repositories/map_projection_system_repository.py b/app/repositories/map_projection_system_repository.py new file mode 100644 index 0000000..6943760 --- /dev/null +++ b/app/repositories/map_projection_system_repository.py @@ -0,0 +1,8 @@ +from app.models import MapProjectionSystemModel + +from . import BaseRepository + + +class MapProjectionSystemRepository(BaseRepository[MapProjectionSystemModel]): + def __init__(self, model): + super().__init__(model) diff --git a/app/repositories/map_source_repository.py b/app/repositories/map_source_repository.py new file mode 100644 index 0000000..8285d7d --- /dev/null +++ b/app/repositories/map_source_repository.py @@ -0,0 +1,8 @@ +from app.models import MapSourceModel + +from . import BaseRepository + + +class MapSourceRepository(BaseRepository[MapSourceModel]): + def __init__(self, model): + super().__init__(model) diff --git a/app/repositories/map_source_usage_repository.py b/app/repositories/map_source_usage_repository.py new file mode 100644 index 0000000..5d3cec8 --- /dev/null +++ b/app/repositories/map_source_usage_repository.py @@ -0,0 +1,28 @@ +from typing import Any, Dict, List +from uuid import UUID + +from fastapi_async_sqlalchemy import db +from sqlalchemy import delete + +from app.models import SourceUsageModel + +from . import BaseRepository + + +class SourceUsageRepository(BaseRepository[SourceUsageModel]): + def __init__(self, model): + super().__init__(model) + + async def bulk_update(self, mapset_id: UUID, data: List[Dict[str, Any]]) -> None: + """Update multiple records.""" + try: + delete_query = delete(self.model).where(self.model.mapset_id == mapset_id) + await db.session.execute(delete_query) + + new_records = [self.model(**item) for item in data] + db.session.add_all(new_records) + + await db.session.commit() + except Exception as e: + await db.session.rollback() + raise e diff --git a/app/repositories/mapset_history_repository.py b/app/repositories/mapset_history_repository.py new file mode 100644 index 0000000..1250e5c --- /dev/null +++ b/app/repositories/mapset_history_repository.py @@ -0,0 +1,8 @@ +from app.models import MapsetHistoryModel + +from . import BaseRepository + + +class MapsetHistoryRepository(BaseRepository[MapsetHistoryModel]): + def __init__(self, model): + super().__init__(model) diff --git a/app/repositories/mapset_repository.py b/app/repositories/mapset_repository.py new file mode 100644 index 0000000..95bedbe --- /dev/null +++ b/app/repositories/mapset_repository.py @@ -0,0 +1,258 @@ +from ast import Dict +from typing import List, Optional, Tuple +from uuid import UUID + +from fastapi_async_sqlalchemy import db +from sqlalchemy import Integer, String, and_, cast, func, or_, select, update +from sqlalchemy.orm import selectinload + +from app.models import ( + ClassificationModel, + MapAccessModel, + MapsetModel, + OrganizationModel, +) +from app.schemas.user_schema import UserSchema + +from . import BaseRepository + + +class MapsetRepository(BaseRepository[MapsetModel]): + def __init__(self, model): + super().__init__(model) + + async def find_all( + self, + user: UserSchema = None, + filters: list = None, + sort: list = ..., + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + landing: bool = False, + ) -> Tuple[List[MapsetModel], int]: + + base_query = select(self.model).distinct() + + base_query = base_query.join(ClassificationModel, self.model.classification_id == ClassificationModel.id) + + if user and user.role.name not in {"administrator", "data_validator"}: + base_query = base_query.join(MapAccessModel, self.model.id == MapAccessModel.mapset_id, isouter=True) + + if (user is None) or landing: + base_query = base_query.filter(ClassificationModel.is_open == True) + base_query = base_query.filter(self.model.is_active == True) + base_query = base_query.filter(self.model.status_validation == "approved") + elif user.role.name not in {"administrator", "data_validator"}: + base_query = base_query.filter( + or_( + # ClassificationModel.is_limited.is_(True), + # ClassificationModel.is_open.is_(True), + and_( + # ClassificationModel.is_secret.is_(True), + self.model.producer_id == user.organization.id, + ), + and_( + # ClassificationModel.is_secret.is_(True), + MapAccessModel.organization_id == user.organization.id, + ), + and_( + # ClassificationModel.is_secret.is_(True), + MapAccessModel.user_id == user.id, + ), + ) + ) + + if filters: + base_query = base_query.filter(*filters) + + if search: + base_query = base_query.join( + OrganizationModel, + self.model.producer_id == OrganizationModel.id, + isouter=True, + ).filter( + or_( + *[ + cast(getattr(self.model, col), String).ilike(f"%{search}%") + for col in self.model.__table__.columns.keys() + ], + OrganizationModel.name.ilike(f"%{search}%"), + ) + ) + + if group_by: + base_query = base_query.group_by(getattr(self.model, group_by)) + + count_query = select(func.count()).select_from(base_query.subquery()) + total = await db.session.scalar(count_query) + + if not sort or sort is ...: + base_query = base_query.order_by(self.model.order.asc()) + else: + base_query = base_query.order_by(*sort) + + base_query = base_query.limit(limit).offset(offset) + + result = await db.session.execute(base_query) + result = result.scalars().all() + + return result, total + + async def find_all_group_by_organization( + self, + user: Optional[UserSchema] = None, + mapset_filters: list = None, + organization_filters: list = None, + sort: list = None, + search: str = "", + limit: int = 100, + offset: int = 0, + ) -> Tuple[List[Dict], int]: + + mapset_filters = mapset_filters or [] + organization_filters = organization_filters or [] + sort = sort or [OrganizationModel.name.asc()] + + if user is None: + base_mapset_query = ( + select(self.model) + .join( + ClassificationModel, + self.model.classification_id == ClassificationModel.id, + ) + .filter(ClassificationModel.is_open.is_(True)) + ) + elif user.role in {"administrator", "data-validator"}: + base_mapset_query = select(self.model) + else: + user_org_id = user.organization.id if user.organization else None + + base_mapset_query = ( + select(self.model) + .join( + ClassificationModel, + self.model.classification_id == ClassificationModel.id, + ) + .outerjoin(MapAccessModel, self.model.id == MapAccessModel.mapset_id) + .filter( + or_( + ClassificationModel.is_open.is_(True), + ClassificationModel.is_limited.is_(True), + and_( + ClassificationModel.is_secret.is_(True), + self.model.producer_id == user.id, + ), + and_( + ClassificationModel.is_secret.is_(True), + MapAccessModel.user_id == user.id, + ), + and_( + ClassificationModel.is_secret.is_(True), + user_org_id is not None, + MapAccessModel.organization_id == user_org_id, + ), + ) + ) + ) + + filtered_mapset_query = base_mapset_query + if mapset_filters: + filtered_mapset_query = filtered_mapset_query.filter(*mapset_filters) + + if search: + search_filters = [] + for col in self.model.__table__.columns.keys(): + if hasattr(self.model, col): + search_filters.append(cast(getattr(self.model, col), String).ilike(f"%{search}%")) + + if search_filters: + filtered_mapset_query = filtered_mapset_query.filter(or_(*search_filters)) + + producer_ids_subquery = select(self.model.producer_id).select_from(filtered_mapset_query.subquery()).distinct() + + org_query = select(OrganizationModel).filter(OrganizationModel.id.in_(producer_ids_subquery)) + + if organization_filters: + org_query = org_query.filter(*organization_filters) + + if search: + org_search_filters = [] + for col in OrganizationModel.__table__.columns.keys(): + if hasattr(OrganizationModel, col): + org_search_filters.append(cast(getattr(OrganizationModel, col), String).ilike(f"%{search}%")) + + if org_search_filters: + org_query = org_query.filter(or_(*org_search_filters)) + + count_query = select(func.count()).select_from( + select(OrganizationModel.id).select_from(org_query.subquery()).distinct() + ) + total = await db.session.scalar(count_query) + + org_query = org_query.order_by(*sort) + + if limit: + org_query = org_query.limit(limit) + if offset: + org_query = org_query.offset(offset) + + org_result = await db.session.execute(org_query) + organizations = org_result.scalars().unique().all() + + org_ids = [org.id for org in organizations] + + if not org_ids: + return [], total + + all_mapsets_query = filtered_mapset_query.filter(self.model.producer_id.in_(org_ids)).options( + selectinload(self.model.classification) + ) + + all_mapsets_result = await db.session.execute(all_mapsets_query) + all_mapsets = all_mapsets_result.scalars().unique().all() + + mapsets_by_org = {} + for mapset in all_mapsets: + if mapset.producer_id not in mapsets_by_org: + mapsets_by_org[mapset.producer_id] = [] + mapsets_by_org[mapset.producer_id].append(mapset) + + result_data = [] + for org in organizations: + org_mapsets = mapsets_by_org.get(org.id, []) + + result_data.append( + { + "id": org.id, + "name": org.name, + "mapsets": org_mapsets, + "found": len(org_mapsets), + } + ) + + return result_data, total + + async def bulk_update_activation(self, mapset_ids: List[UUID], is_active: bool) -> None: + for mapset_id in mapset_ids: + await db.session.execute(update(self.model).where(self.model.id == mapset_id).values(is_active=is_active)) + await db.session.commit() + + async def increment_view_count(self, mapset_id: UUID) -> None: + query = ( + update(self.model) + .where(self.model.id == mapset_id) + .values(view_count=self.model.view_count + 1) + ) + await db.session.execute(query) + await db.session.commit() + + async def increment_download_count(self, mapset_id: UUID) -> None: + query = ( + update(self.model) + .where(self.model.id == mapset_id) + .values(download_count=self.model.download_count + 1) + ) + await db.session.execute(query) + await db.session.commit() diff --git a/app/repositories/news_repository.py b/app/repositories/news_repository.py new file mode 100644 index 0000000..09928cd --- /dev/null +++ b/app/repositories/news_repository.py @@ -0,0 +1,19 @@ +from typing import List +from uuid import UUID + +from fastapi_async_sqlalchemy import db +from sqlalchemy import update + +from app.models import NewsModel + +from . import BaseRepository + + +class NewsRepository(BaseRepository[NewsModel]): + def __init__(self, model): + super().__init__(model) + + async def bulk_update_activation(self, news_ids: List[UUID], is_active: bool) -> None: + for news_id in news_ids: + await db.session.execute(update(self.model).where(self.model.id == news_id).values(is_active=is_active)) + await db.session.commit() diff --git a/app/repositories/organization_repository.py b/app/repositories/organization_repository.py new file mode 100644 index 0000000..a257fc2 --- /dev/null +++ b/app/repositories/organization_repository.py @@ -0,0 +1,318 @@ +from typing import Any, Dict, List, Optional, Tuple, override +from uuid import UUID + +from fastapi_async_sqlalchemy import db +from sqlalchemy import ( + Integer, + Numeric, + String, + Unicode, + UnicodeText, + and_, + cast, + exists, + func, + or_, + select, + desc, + text, + update as sqlalchemy_update +) + +from app.models.classification_model import ClassificationModel +from app.models.map_access_model import MapAccessModel +from app.models.mapset_model import MapsetModel +from app.models.organization_model import OrganizationModel +from app.schemas.user_schema import UserSchema + +from . import BaseRepository + + +class OrganizationRepository(BaseRepository[OrganizationModel]): + def __init__(self, model, mapset_model: MapsetModel): + super().__init__(model) + self.mapset_model = mapset_model + + async def flag_delete_organization(self, id): + return await self.flag_delete_organization(id) + + async def find_by_name(self, name: str, sensitive: bool = False): + if not sensitive: + name = name.lower() + + query = select(self.model) + if not sensitive: + query = query.where(self.model.name == name) + else: + query = query.where(self.model.name.ilike(f"%{name}%")) + + result = await db.session.execute(query) + + return result.scalar_one_or_none() + + async def find_all( + self, + user: UserSchema | None, + filters: list, + sort: list | None = None, + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + landing: bool = False, + ) -> Tuple[List[OrganizationModel], int]: + """Find all records with pagination.""" + if sort is None: + sort = [] + + mapset_count = func.count(self.mapset_model.id).label("count_mapset") + + base_query = select( + self.model.id, + self.model.name, + self.model.description, + self.model.thumbnail, + self.model.address, + self.model.phone_number, + self.model.email, + self.model.website, + mapset_count, + self.model.is_active, + self.model.is_deleted, + self.model.created_at, + self.model.modified_at, + ).select_from(self.model) + + # Use simple outerjoin first, then apply WHERE conditions + # This ensures organizations without mapsets are still included + base_query = base_query.outerjoin( + self.mapset_model, + self.model.id == self.mapset_model.producer_id + ) + base_query = base_query.outerjoin( + ClassificationModel, + self.mapset_model.classification_id == ClassificationModel.id, + ) + + # Apply mapset-level filters only to the mapset records, not to the organization join + mapset_conditions = [ + or_( + self.mapset_model.id.is_(None), # Allow organizations without mapsets + and_( + self.mapset_model.is_active.is_(True), + self.mapset_model.is_deleted.is_(False), + ) + ) + ] + + # Add user-specific filters for mapsets + # When landing=True, count all mapsets without filtering by user organization + if (user is None) or landing: + mapset_conditions.append( + or_( + self.mapset_model.id.is_(None), # Organizations without mapsets + and_( + self.mapset_model.status_validation == "approved", + ClassificationModel.is_open.is_(True) + ) + ) + ) + elif user.role not in {"administrator", "data_validator"}: + # When landing=False and user is not admin, filter by user organization + base_query = base_query.outerjoin( + MapAccessModel, + and_( + self.mapset_model.id == MapAccessModel.mapset_id, + or_( + MapAccessModel.organization_id == user.organization.id, + MapAccessModel.user_id == user.id, + ), + ), + ) + mapset_conditions.append( + or_( + self.mapset_model.id.is_(None), # Organizations without mapsets + ClassificationModel.is_limited.is_(True), + ClassificationModel.is_open.is_(True), + and_( + ClassificationModel.is_secret.is_(True), + self.mapset_model.producer_id == user.organization.id, + ), + ) + ) + + # Apply all mapset conditions + base_query = base_query.where(and_(*mapset_conditions)) + + if hasattr(self.model, "is_deleted"): + base_query = base_query.where(self.model.is_deleted.is_(False)) + + if filters: + base_query = base_query.where(*filters) + + if search: + search_filters = [] + for col in self.model.__table__.columns.keys(): + column = getattr(self.model, col) + if isinstance(column.type, (String, Unicode, UnicodeText)): + search_filters.append(column.ilike(f"%{search}%")) + elif isinstance(column.type, (Integer, Numeric)): + try: + num_val = float(search) + search_filters.append(cast(column, String) == str(num_val)) + except (ValueError, TypeError): + pass + + if search_filters: + base_query = base_query.where(or_(*search_filters)) + + group_columns = [self.model.id] + if group_by and hasattr(self.model, group_by): + group_col = getattr(self.model, group_by) + if group_col not in group_columns: + group_columns.append(group_col) + + base_query = base_query.group_by(*group_columns) + + count_query = select(func.count(self.model.id)).select_from(self.model) + + if hasattr(self.model, "is_deleted"): + count_query = count_query.where(self.model.is_deleted.is_(False)) + + if filters: + count_query = count_query.where(*filters) + + if search and search_filters: + count_query = count_query.where(or_(*search_filters)) + + # For count query, we don't need to filter organizations based on mapset availability + # This allows organizations with 0 mapsets to be included in the count + # The filtering logic should be the same as the main query structure + # but we don't need the mapset join conditions for counting organizations + pass + + total = await db.session.scalar(count_query) + + if sort: + base_query = base_query.order_by(*sort) + else: + base_query = base_query.order_by(desc(mapset_count)) + + base_query = base_query.limit(limit).offset(offset) + + result = await db.session.execute(base_query) + items = result.mappings().all() + + return items, total + + @override + async def update(self, id: UUID, data: Dict[str, Any], refresh: bool = True) -> Optional[OrganizationModel]: + """Update record with optimization.""" + clean_data = {k: v for k, v in data.items() if v is not None} + + if not clean_data: + return await self.find_by_id(None, id) if refresh else None + + query = ( + sqlalchemy_update(self.model) + .where(self.model.id == id) + .values(**clean_data) + .execution_options(synchronize_session="fetch") + ) + + result = await db.session.execute(query) + await db.session.commit() + + if result.rowcount == 0: + return None + + return await self.find_by_id(None, id) if refresh else None + + @override + async def find_by_id(self, user: UserSchema | None, id: UUID) -> Optional[OrganizationModel]: + if user is None: + mapset_condition = and_( + self.mapset_model.is_active.is_(True), + self.mapset_model.is_deleted.is_(False), + self.mapset_model.status_validation == "approved", + self.mapset_model.producer_id == self.model.id, + ) + + mapset_filter = or_( + mapset_condition, + self.mapset_model.id.is_(None) + ) + elif user.role in {"administrator", "data_validator"}: + mapset_condition = and_( + self.mapset_model.is_active.is_(True), + self.mapset_model.is_deleted.is_(False), + self.mapset_model.producer_id == self.model.id, + ) + + mapset_filter = or_( + mapset_condition, + self.mapset_model.id.is_(None) + ) + else: + mapset_condition = and_( + or_( + ClassificationModel.is_limited.is_(True), + ClassificationModel.is_open.is_(True), + and_( + ClassificationModel.is_secret.is_(True), + self.mapset_model.producer_id == user.organization.id, + ), + and_( + ClassificationModel.is_secret.is_(True), + MapAccessModel.organization_id == user.organization.id, + ), + and_( + ClassificationModel.is_secret.is_(True), + MapAccessModel.user_id == user.id, + ), + ), + self.mapset_model.is_active.is_(True), + self.mapset_model.is_deleted.is_(False), + self.mapset_model.producer_id == self.model.id, + ) + mapset_filter = or_( + mapset_condition, + self.mapset_model.id.is_(None) + ) + + query = ( + select( + self.model.id, + self.model.name, + self.model.description, + self.model.thumbnail, + self.model.address, + self.model.phone_number, + self.model.email, + self.model.website, + func.count(self.mapset_model.id).label("count_mapset"), + self.model.is_active, + self.model.is_deleted, + self.model.created_at, + self.model.modified_at, + ) + .outerjoin(self.mapset_model, self.model.id == self.mapset_model.producer_id) + .outerjoin( + ClassificationModel, + self.mapset_model.classification_id == ClassificationModel.id, + ) + ) + + if user is not None and user.role not in {"administrator", "data_validator"}: + query = query.outerjoin(MapAccessModel, self.mapset_model.id == MapAccessModel.mapset_id) + + if user is None or user.role not in {"administrator", "data_validator"}: + query = query.where(mapset_filter) + + if hasattr(self.model, "is_deleted"): + query = query.filter(self.model.is_deleted.is_(False)) + + query = query.filter(self.model.id == id) + query = query.group_by(self.model.id) + result = await db.session.execute(query) + return result.mappings().one_or_none() diff --git a/app/repositories/regional_repository.py b/app/repositories/regional_repository.py new file mode 100644 index 0000000..0794867 --- /dev/null +++ b/app/repositories/regional_repository.py @@ -0,0 +1,8 @@ +from app.models import RegionalModel + +from . import BaseRepository + + +class RegionalRepository(BaseRepository[RegionalModel]): + def __init__(self, model): + super().__init__(model) diff --git a/app/repositories/role_repository.py b/app/repositories/role_repository.py new file mode 100644 index 0000000..b637875 --- /dev/null +++ b/app/repositories/role_repository.py @@ -0,0 +1,24 @@ +from typing import List + +from fastapi_async_sqlalchemy import db +from sqlalchemy import select + +from app.models import RoleModel + +from . import BaseRepository + + +class RoleRepository(BaseRepository[RoleModel]): + def __init__(self, model): + super().__init__(model) + + async def find_by_name(self, name: str) -> RoleModel: + """Find record by name.""" + query = select(self.model).where(self.model.name == name) + result = await db.session.execute(query) + return result.scalar_one_or_none() + + async def get_list_by_names(self, name: List[str]) -> List[RoleModel]: + query = select(self.model).filter(self.model.name.in_(name)) + result = await db.session.execute(query) + return result.scalars().all() diff --git a/app/repositories/token_repository.py b/app/repositories/token_repository.py new file mode 100644 index 0000000..eca5305 --- /dev/null +++ b/app/repositories/token_repository.py @@ -0,0 +1,39 @@ +from datetime import datetime + +from fastapi_async_sqlalchemy import db +from pytz import timezone +from sqlalchemy import select +from uuid6 import UUID + +from app.core.config import settings +from app.models import RefreshTokenModel +from app.repositories import BaseRepository + + +class TokenRepository(BaseRepository[RefreshTokenModel]): + def __init__(self, model): + super().__init__(model) + + async def find_valid_token(self, token: str, user_id: UUID): + query = select(self.model).where( + self.model.token == token, + self.model.user_id == str(user_id), + self.model.expires_at > datetime.now(timezone(settings.TIMEZONE)), + self.model.revoked == False, + ) + result = await db.session.execute(query) + return result.scalars().first() + + async def revoke_token(self, token: str): + token_obj = await self.find_by_token(token) + if token_obj: + token_obj.revoked = True + db.session.add(token_obj) + await db.session.commit() + return True + return False + + async def find_by_token(self, token: str): + query = select(self.model).where(self.model.token == token) + result = await db.session.execute(query) + return result.scalars().first() diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py new file mode 100644 index 0000000..629a25e --- /dev/null +++ b/app/repositories/user_repository.py @@ -0,0 +1,41 @@ +from typing import List + +from fastapi_async_sqlalchemy import db +from sqlalchemy import select, update +from uuid6 import UUID + +from app.models import UserModel + +from . import BaseRepository + + +class UserRepository(BaseRepository[UserModel]): + def __init__(self, model): + super().__init__(model) + + async def find_by_username(self, username: str) -> UserModel | None: + query = select(self.model).filter(self.model.username == username) + result = await db.session.execute(query) + return result.scalar_one_or_none() + + async def find_by_email(self, email: str) -> UserModel | None: + query = select(self.model).filter(self.model.email == email) + result = await db.session.execute(query) + return result.scalar_one_or_none() + + async def find_by_id(self, id: UUID) -> UserModel | None: + query = select(self.model).filter(self.model.id == id) + result = await db.session.execute(query) + return result.scalar_one_or_none() + + async def find_all_ids(self, list_id: List) -> List[UserModel]: + query = select(self.model.id) + query = query.where(self.model.id.in_(list_id)) + + result = await db.session.execute(query) + return result.scalars().all() + + async def bulk_update_activation(self, user_ids: List[UUID], is_active: bool) -> None: + for user_id in user_ids: + await db.session.execute(update(self.model).where(self.model.id == user_id).values(is_active=is_active)) + await db.session.commit() diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..6deeed8 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1,80 @@ +from .category_schema import CategoryCreateSchema, CategorySchema, CategoryUpdateSchema +from .classification_schema import ( + ClassificationCreateSchema, + ClassificationSchema, + ClassificationUpdateSchema, +) +from .credential_schema import ( + CredentialCreateSchema, + CredentialSchema, + CredentialUpdateSchema, +) +from .file_schema import FileSchema +from .map_access_schema import ( + MapAccessCreateSchema, + MapAccessSchema, + MapAccessUpdateSchema, +) +from .map_projection_system_schema import ( + MapProjectionSystemCreateSchema, + MapProjectionSystemSchema, + MapProjectionSystemUpdateSchema, +) +from .map_source_schema import ( + MapSourceCreateSchema, + MapSourceSchema, + MapSourceUpdateSchema, +) +from .mapset_history_schema import MapsetHistoryCreateSchema, MapsetHistorySchema +from .mapset_schema import MapsetCreateSchema, MapsetSchema, MapsetUpdateSchema +from .news_schema import NewsCreateSchema, NewsSchema, NewsUpdateSchema +from .organization_schema import ( + OrganizationCreateSchema, + OrganizationSchema, + OrganizationUpdateSchema, +) +from .regional_schema import RegionalCreateSchema, RegionalSchema, RegionalUpdateSchema +from .role_schema import RoleCreateSchema, RoleSchema, RoleUpdateSchema +from .user_schema import UserCreateSchema, UserSchema, UserUpdateSchema + +__all__ = [ + "OrganizationSchema", + "OrganizationCreateSchema", + "OrganizationUpdateSchema", + "UserSchema", + "UserCreateSchema", + "UserUpdateSchema", + "RoleSchema", + "RoleCreateSchema", + "RoleUpdateSchema", + "NewsSchema", + "NewsCreateSchema", + "NewsUpdateSchema", + "FileSchema", + "CredentialSchema", + "CredentialCreateSchema", + "CredentialUpdateSchema", + "MapsetSchema", + "MapsetCreateSchema", + "MapsetUpdateSchema", + "MapSourceSchema", + "MapSourceCreateSchema", + "MapSourceUpdateSchema", + "MapProjectionSystemSchema", + "MapProjectionSystemCreateSchema", + "MapProjectionSystemUpdateSchema", + "MapAccessSchema", + "MapAccessCreateSchema", + "MapAccessUpdateSchema", + "MapsetHistorySchema", + "MapsetHistoryCreateSchema", + "CategoryCreateSchema", + "CategorySchema", + "CategoryUpdateSchema", + "ClassificationSchema", + "ClassificationCreateSchema", + "ClassificationUpdateSchema", + "RegionalSchema", + "RegionalCreateSchema", + "RegionalUpdateSchema", +] diff --git a/app/schemas/base.py b/app/schemas/base.py new file mode 100644 index 0000000..b880058 --- /dev/null +++ b/app/schemas/base.py @@ -0,0 +1,28 @@ +from typing import Generic, List, TypeVar + +from pydantic import BaseModel, ConfigDict + +from app.utils.helpers import orjson_dumps + +T = TypeVar("T") + + +class BaseSchema(BaseModel): + """Base Pydantic model with orjson configuration.""" + + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + + def model_dump_json(self, **kwargs): + """Override default json serialization to use orjson.""" + return orjson_dumps(self.model_dump(**kwargs)) + + +class PaginatedResponse(BaseSchema, Generic[T]): + items: List[T] + total: int + limit: int + offset: int + has_more: bool diff --git a/app/schemas/category_schema.py b/app/schemas/category_schema.py new file mode 100644 index 0000000..683b645 --- /dev/null +++ b/app/schemas/category_schema.py @@ -0,0 +1,30 @@ +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class CategorySchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] = None + thumbnail: Optional[str] = None + count_mapset: int = 0 + is_active: bool = True + + +class CategoryCreateSchema(BaseSchema): + name: str = Field(..., min_length=1) + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: bool = True + + +class CategoryUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=1) + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: Optional[bool] = None diff --git a/app/schemas/classification_schema.py b/app/schemas/classification_schema.py new file mode 100644 index 0000000..5a74587 --- /dev/null +++ b/app/schemas/classification_schema.py @@ -0,0 +1,29 @@ +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class ClassificationSchema(BaseSchema): + id: UUID7Field + name: str + is_open: bool + is_limited: bool + is_secret: bool + + +class ClassificationCreateSchema(BaseSchema): + name: str + is_open: bool + is_limited: bool + is_secret: bool + + +class ClassificationUpdateSchema(BaseSchema): + name: Optional[str] = Field(None) + is_open: Optional[bool] = Field(None) + is_limited: Optional[bool] = Field(None) + is_secret: Optional[bool] = Field(None) diff --git a/app/schemas/count_schema.py b/app/schemas/count_schema.py new file mode 100644 index 0000000..baf1104 --- /dev/null +++ b/app/schemas/count_schema.py @@ -0,0 +1,9 @@ +from app.schemas.base import BaseSchema + + +class CountSchema(BaseSchema): + mapset_count: int + organization_count: int + visitor_count: int + metadata_count: int + download_count: int diff --git a/app/schemas/credential_schema.py b/app/schemas/credential_schema.py new file mode 100644 index 0000000..0debb1e --- /dev/null +++ b/app/schemas/credential_schema.py @@ -0,0 +1,212 @@ +from datetime import datetime +from typing import Any, Dict, Optional + +from pydantic import Field, field_validator + +from app.core.data_types import UUID7Field +from app.core.exceptions import UnprocessableEntity + +from .base import BaseSchema + + +class CredentialBase(BaseSchema): + name: str = Field(..., description="Nama kredensial") + description: Optional[str] = Field(None, description="Deskripsi kredensial") + credential_type: str = Field(..., description="Tipe kredensial ('database', 'api', 'minio', dll)") + credential_metadata: Optional[Dict[str, Any]] = Field( + default={}, description="Metadata tidak sensitif (tidak dienkripsi)" + ) + is_default: bool = Field( + default=False, description="Apakah kredensial ini digunakan sebagai default untuk tipenya" + ) + + +class CredentialSchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] + credential_type: str + credential_metadata: Optional[Dict[str, Any]] + is_default: bool + is_active: bool + created_by: UUID7Field + updated_by: Optional[UUID7Field] + created_at: datetime + updated_at: Optional[datetime] + last_used_at: Optional[datetime] + last_used_by: Optional[UUID7Field] + + +class CredentialWithSensitiveDataSchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] + decrypted_data: Dict[str, Any] + credential_type: str + credential_metadata: Optional[Dict[str, Any]] + is_default: bool + is_active: bool + created_by: UUID7Field + updated_by: Optional[UUID7Field] + created_at: datetime + updated_at: Optional[datetime] + last_used_at: Optional[datetime] + last_used_by: Optional[UUID7Field] + + +class CredentialCreateSchema(CredentialBase): + name: str = Field(..., description="Nama kredensial") + description: Optional[str] = Field(None, description="Deskripsi kredensial") + credential_type: str = Field(..., description="Tipe kredensial ('database', 'api', 'minio', dll)") + credential_metadata: Optional[Dict[str, Any]] = Field( + default={}, description="Metadata tidak sensitif (tidak dienkripsi)" + ) + is_default: bool = Field( + default=False, description="Apakah kredensial ini digunakan sebagai default untuk tipenya" + ) + is_active: bool = Field(default=True, description="Status aktif kredensial") + sensitive_data: Dict[str, Any] = Field(..., description="Data sensitif yang akan dienkripsi") + + @field_validator("credential_type") + @classmethod + def validate_credential_type(cls, v): + allowed_types = {"database", "api", "minio", "ssh", "smtp", "ftp", "geoserver", "geonetwork"} + if v not in allowed_types: + raise UnprocessableEntity(f'credential_type harus salah satu dari: {", ".join(allowed_types)}') + return v + + @field_validator("sensitive_data") + @classmethod + def validate_sensitive_data(cls, v, values): + """Validasi data sensitif berdasarkan tipe kredensial.""" + credential_type = values.data.get("credential_type", "") + + # Validasi untuk database + if credential_type == "database": + required_fields = {"host", "port", "username", "password", "database_name"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for database: {', '.join(missing)}") + + # Validasi untuk MinIO + elif credential_type == "minio": + required_fields = {"endpoint", "access_key", "secret_key", "secure", "bucket_name"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for minio: {', '.join(missing)}") + + # Validasi untuk API + elif credential_type == "api": + required_fields = {"base_url", "api_key"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for api: {', '.join(missing)}") + + # Validasi untuk SSH + elif credential_type == "ssh": + if not ("password" in v or "private_key" in v): + raise UnprocessableEntity("Either 'password' or 'private_key' is required for SSH credentials") + + required_fields = {"host", "port", "username"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for ssh: {', '.join(missing)}") + + # Validasi untuk SMTP + elif credential_type == "smtp": + required_fields = {"host", "port", "username", "password", "use_tls"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for smtp: {', '.join(missing)}") + + # Validasi untuk FTP + elif credential_type == "ftp": + required_fields = {"host", "port", "username", "password"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for ftp: {', '.join(missing)}") + + elif credential_type == "server": + required_fields = {"host", "port", "username", "password"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for server: {', '.join(missing)}") + + return v + + +class CredentialUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, description="Nama kredensial") + description: Optional[str] = Field(None, description="Deskripsi kredensial") + credential_type: Optional[str] = Field(None, description="Tipe kredensial ('database', 'api', 'minio', dll)") + credential_metadata: Optional[Dict[str, Any]] = Field( + default={}, description="Metadata tidak sensitif (tidak dienkripsi)" + ) + is_default: Optional[bool] = Field( + default=False, description="Apakah kredensial ini digunakan sebagai default untuk tipenya" + ) + is_active: Optional[bool] = Field(default=True, description="Status aktif kredensial") + sensitive_data: Optional[Dict[str, Any]] = Field(None, description="Data sensitif yang akan dienkripsi") + + @field_validator("credential_type") + @classmethod + def validate_credential_type(cls, v): + allowed_types = {"database", "api", "minio", "ssh", "smtp", "ftp"} + if v not in allowed_types: + raise UnprocessableEntity(f'credential_type harus salah satu dari: {", ".join(allowed_types)}') + return v + + @field_validator("sensitive_data") + @classmethod + def validate_sensitive_data(cls, v, values): + """Validasi data sensitif berdasarkan tipe kredensial.""" + credential_type = values.data.get("credential_type", "") + + # Validasi untuk database + if credential_type == "database": + required_fields = {"host", "port", "username", "password", "database_name"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for database: {', '.join(missing)}") + + # Validasi untuk MinIO + elif credential_type == "minio": + required_fields = {"endpoint", "access_key", "secret_key", "secure", "bucket_name"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for minio: {', '.join(missing)}") + + # Validasi untuk API + elif credential_type == "api": + required_fields = {"base_url", "api_key"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for api: {', '.join(missing)}") + + # Validasi untuk SSH + elif credential_type == "ssh": + if not ("password" in v or "private_key" in v): + raise UnprocessableEntity("Either 'password' or 'private_key' is required for SSH credentials") + + required_fields = {"host", "port", "username"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for ssh: {', '.join(missing)}") + + # Validasi untuk SMTP + elif credential_type == "smtp": + required_fields = {"host", "port", "username", "password", "use_tls"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for smtp: {', '.join(missing)}") + + # Validasi untuk FTP + elif credential_type == "ftp": + required_fields = {"host", "port", "username", "password"} + missing + + elif credential_type == "server": + required_fields = {"host", "port", "username", "password"} + missing = required_fields - set(v.keys()) + if missing: + raise UnprocessableEntity(f"Missing required fields for server: {', '.join(missing)}") diff --git a/app/schemas/error_schema.py b/app/schemas/error_schema.py new file mode 100644 index 0000000..92a6e08 --- /dev/null +++ b/app/schemas/error_schema.py @@ -0,0 +1,5 @@ +from .base import BaseSchema + + +class ErrorResponse(BaseSchema): + message: str diff --git a/app/schemas/feedback_schema.py b/app/schemas/feedback_schema.py new file mode 100644 index 0000000..7e27b4c --- /dev/null +++ b/app/schemas/feedback_schema.py @@ -0,0 +1,34 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + + +class FeedbackBase(BaseModel): + score: int + tujuan_tercapai: Optional[bool] = True + tujuan_ditemukan: Optional[bool] = True + tujuan: Optional[str] = None + sektor: Optional[str] = None + email: Optional[str] = None + saran: Optional[str] = None + source_url: Optional[str] = None + source_access: Optional[str] = None + notes: Optional[str] = None + gender: Optional[int] = None + + +class FeedbackCreateSchema(FeedbackBase): + pass + + +class FeedbackUpdateSchema(FeedbackBase): + score: Optional[int] = None + + +class FeedbackSchema(FeedbackBase): + id: int + datetime: datetime + + class Config: + from_attributes = True diff --git a/app/schemas/file_schema.py b/app/schemas/file_schema.py new file mode 100644 index 0000000..2f10924 --- /dev/null +++ b/app/schemas/file_schema.py @@ -0,0 +1,33 @@ +from datetime import datetime +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema +from .user_schema import UserSchema + + +class FileSchema(BaseSchema): + id: UUID7Field + object_name: str + uploaded_by: UserSchema + created_at: datetime + modified_at: Optional[datetime] = None + + +class FileCreateSchema(BaseSchema): + filename: str + content_type: str + size: int + description: Optional[str] = None + url: str + + +class FileUpdateSchema(BaseSchema): + filename: Optional[str] = Field(None, title="File Name") + content_type: Optional[str] = Field(None, title="Content Type") + size: Optional[int] = Field(None, title="File Size") + description: Optional[str] = Field(None, title="File Description") + url: Optional[str] = Field(None, title="File URL") diff --git a/app/schemas/map_access_schema.py b/app/schemas/map_access_schema.py new file mode 100644 index 0000000..d6b7c6c --- /dev/null +++ b/app/schemas/map_access_schema.py @@ -0,0 +1,30 @@ +from typing import Optional + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class MapAccessSchema(BaseSchema): + id: UUID7Field + mapset_id: UUID7Field + user_id: Optional[UUID7Field] = None + organization_id: Optional[UUID7Field] = None + can_read: bool + can_write: bool + can_delete: bool + + +class MapAccessCreateSchema(BaseSchema): + mapset_id: UUID7Field + user_id: Optional[UUID7Field] = None + organization_id: Optional[UUID7Field] = None + can_read: bool + can_write: bool + can_delete: bool + + +class MapAccessUpdateSchema(BaseSchema): + can_read: Optional[bool] = None + can_write: Optional[bool] = None + can_delete: Optional[bool] = None diff --git a/app/schemas/map_projection_system_schema.py b/app/schemas/map_projection_system_schema.py new file mode 100644 index 0000000..feb80ae --- /dev/null +++ b/app/schemas/map_projection_system_schema.py @@ -0,0 +1,20 @@ +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class MapProjectionSystemSchema(BaseSchema): + id: UUID7Field + name: str + + +class MapProjectionSystemCreateSchema(BaseSchema): + name: str = Field(..., min_length=1, max_length=50) + + +class MapProjectionSystemUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=1, max_length=50) diff --git a/app/schemas/map_source_schema.py b/app/schemas/map_source_schema.py new file mode 100644 index 0000000..eea26cd --- /dev/null +++ b/app/schemas/map_source_schema.py @@ -0,0 +1,38 @@ +from datetime import datetime +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field +from app.schemas import CredentialSchema + +from .base import BaseSchema + + +class MapSourceSchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] + credential: CredentialSchema + url: Optional[str] + is_active: bool + is_deleted: bool + created_at: datetime + updated_at: Optional[datetime] + + +class MapSourceCreateSchema(BaseSchema): + name: str = Field(..., min_length=1, max_length=50) + description: Optional[str] = Field(None) + url: Optional[str] + credential_id: UUID7Field + is_active: bool = True + + +class MapSourceUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=1, max_length=50) + description: Optional[str] = Field(None) + url: Optional[str] = Field(None) + credential_id: Optional[UUID7Field] + is_active: Optional[bool] = None + is_deleted: Optional[bool] = None diff --git a/app/schemas/mapset_history_schema.py b/app/schemas/mapset_history_schema.py new file mode 100644 index 0000000..b64e261 --- /dev/null +++ b/app/schemas/mapset_history_schema.py @@ -0,0 +1,24 @@ +from datetime import datetime +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field +from app.schemas.user_schema import UserSchema + +from .base import BaseSchema + + +class MapsetHistorySchema(BaseSchema): + id: UUID7Field + mapset_id: UUID7Field + validation_type: str + notes: Optional[str] + timestamp: datetime + user_info: UserSchema = Field(alias="user") + + +class MapsetHistoryCreateSchema(BaseSchema): + mapset_id: UUID7Field + validation_type: str = Field(..., min_length=1) + notes: Optional[str] = None diff --git a/app/schemas/mapset_schema.py b/app/schemas/mapset_schema.py new file mode 100644 index 0000000..d08859f --- /dev/null +++ b/app/schemas/mapset_schema.py @@ -0,0 +1,98 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field +from app.schemas.category_schema import CategorySchema +from app.schemas.classification_schema import ClassificationSchema +from app.schemas.map_projection_system_schema import MapProjectionSystemSchema +from app.schemas.map_source_schema import MapSourceSchema +from app.schemas.organization_schema import OrganizationWithMapsetSchema +from app.schemas.regional_schema import RegionalSchema + +from .base import BaseSchema + + +class MapsetSchema(BaseSchema): + id: UUID7Field + name: str + description: str + scale: Optional[str] + layer_url: Optional[str] + metadata_url: Optional[str] + status_validation: Optional[str] + classification: str + data_status: str + data_update_period: Optional[str] + data_version: Optional[str] + coverage_level: Optional[str] + coverage_area: Optional[str] + layer_type: Optional[str] + category: CategorySchema + projection_system: MapProjectionSystemSchema + producer: OrganizationWithMapsetSchema + regional: Optional[RegionalSchema] + sources: Optional[List[MapSourceSchema]] = Field([]) + classification: ClassificationSchema + view_count: int + download_count: int + is_popular: bool + is_active: bool + created_at: datetime + updated_at: datetime + + +class MapsetByOrganizationSchema(BaseSchema): + id: UUID7Field + name: str + found: int + mapsets: List[MapsetSchema] + + +class MapsetCreateSchema(BaseSchema): + name: str + description: Optional[str] = Field(None) + scale: Optional[str] = Field(None) + layer_url: str + metadata_url: Optional[str] = Field(None) + status_validation: str + layer_type: Optional[str] = Field(None) + projection_system_id: UUID7Field + category_id: UUID7Field + classification_id: UUID7Field + source_id: Optional[List[UUID7Field]] = Field(None) + regional_id: UUID7Field + producer_id: UUID7Field + data_status: str + data_update_period: Optional[str] = Field(default=None) + data_version: Optional[str] = Field(default=None) + coverage_level: Optional[str] = Field(default=None) + coverage_area: Optional[str] = Field(default=None) + is_popular: bool = Field(default=False) + is_active: bool = Field(default=True) + notes: Optional[str] = Field(None) + + +class MapsetUpdateSchema(BaseSchema): + name: Optional[str] = Field(None) + description: Optional[str] = Field(None) + scale: Optional[str] = Field(None) + layer_url: Optional[str] = Field(None) + metadata_url: Optional[str] = Field(None) + status_validation: Optional[str] = Field(None) + layer_type: Optional[str] = Field(None) + projection_system_id: Optional[UUID7Field] = Field(None) + category_id: Optional[UUID7Field] = Field(None) + classification_id: Optional[UUID7Field] = Field(None) + source_id: Optional[List[UUID7Field]] = Field(None) + regional_id: Optional[UUID7Field] = Field(None) + producer_id: Optional[UUID7Field] = Field(None) + data_status: Optional[str] = Field(None) + data_update_period: Optional[str] = Field(None) + data_version: Optional[str] = Field(None) + coverage_level: Optional[str] = Field(None) + coverage_area: Optional[str] = Field(None) + is_popular: Optional[bool] = Field(None) + is_active: Optional[bool] = Field(None) + notes: Optional[str] = Field(None) diff --git a/app/schemas/news_schema.py b/app/schemas/news_schema.py new file mode 100644 index 0000000..59127ac --- /dev/null +++ b/app/schemas/news_schema.py @@ -0,0 +1,29 @@ +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class NewsSchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: bool = True + + +class NewsCreateSchema(BaseSchema): + name: str = Field(..., min_length=1) + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: bool = True + + +class NewsUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=1) + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: Optional[bool] = None diff --git a/app/schemas/organization_schema.py b/app/schemas/organization_schema.py new file mode 100644 index 0000000..d77944b --- /dev/null +++ b/app/schemas/organization_schema.py @@ -0,0 +1,57 @@ +from datetime import datetime +from typing import Optional + +from pydantic import EmailStr, Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class OrganizationSchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] + thumbnail: Optional[str] + address: Optional[str] + phone_number: Optional[str] + email: Optional[EmailStr] + website: Optional[str] + count_mapset: int = 0 + is_active: bool + created_at: datetime + modified_at: Optional[datetime] + + +class OrganizationWithMapsetSchema(BaseSchema): + id: UUID7Field + name: str + description: Optional[str] + thumbnail: Optional[str] + address: Optional[str] + phone_number: Optional[str] + email: Optional[EmailStr] + website: Optional[str] + + +class OrganizationCreateSchema(BaseSchema): + name: str = Field(..., min_length=1, max_length=100) + description: Optional[str] = Field(None, min_length=1, max_length=500) + thumbnail: Optional[str] = Field(None, min_length=1, max_length=255) + address: Optional[str] = Field(None, min_length=1, max_length=255) + phone_number: Optional[str] = Field(None, min_length=1, max_length=15) + email: Optional[EmailStr] = Field(None, max_length=100) + website: Optional[str] = Field(None, min_length=1, max_length=100) + is_active: Optional[bool] = Field(True) + + +class OrganizationUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=1, max_length=100) + description: Optional[str] = Field(None, min_length=1, max_length=500) + thumbnail: Optional[str] = Field(None, min_length=1, max_length=500) + phone_number: Optional[str] = Field(None, min_length=1, max_length=15) + address: Optional[str] = Field(None, min_length=1, max_length=500) + email: Optional[EmailStr] = Field(None) + website: Optional[str] = Field(None, min_length=1, max_length=100) + is_active: Optional[bool] = Field(None) + is_deleted: Optional[bool] = Field(None) diff --git a/app/schemas/regional_schema.py b/app/schemas/regional_schema.py new file mode 100644 index 0000000..9b897b7 --- /dev/null +++ b/app/schemas/regional_schema.py @@ -0,0 +1,32 @@ +from typing import Optional + +from pydantic import Field + +from app.core.data_types import UUID7Field + +from .base import BaseSchema + + +class RegionalSchema(BaseSchema): + id: UUID7Field + code: str + name: str + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: bool = True + + +class RegionalCreateSchema(BaseSchema): + code: str = Field(..., min_length=1, max_length=10) + name: str = Field(..., min_length=1, max_length=50) + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: bool = True + + +class RegionalUpdateSchema(BaseSchema): + code: Optional[str] = Field(None, min_length=1, max_length=10) + name: Optional[str] = Field(None, min_length=1, max_length=50) + description: Optional[str] = None + thumbnail: Optional[str] = None + is_active: Optional[bool] = None diff --git a/app/schemas/role_schema.py b/app/schemas/role_schema.py new file mode 100644 index 0000000..a2b1a75 --- /dev/null +++ b/app/schemas/role_schema.py @@ -0,0 +1,55 @@ +from typing import Optional + +from pydantic import Field, field_validator + +from app.core.data_types import UUID7Field +from app.core.exceptions import UnprocessableEntity + +from .base import BaseSchema + + +class RoleSchema(BaseSchema): + id: UUID7Field + name: str = Field(..., min_length=1, max_length=50) + description: Optional[str] = Field(None) + is_active: bool = True + + +class RoleCreateSchema(BaseSchema): + name: str = Field(..., min_length=1, max_length=50) + description: Optional[str] = Field(None) + is_active: bool = True + + @field_validator("name") + @classmethod + def validate_name(cls, value): + if value is None: + return value + + valid_role = ["administrator", "data_validator", "data_manager", "data_viewer"] + if value not in valid_role: + raise UnprocessableEntity( + f"Role name must be one of the following: administrator, {', '.join(valid_role)}" + ) + + return value + + +class RoleUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=1, max_length=50) + description: Optional[str] = Field(None) + is_active: Optional[bool] = None + + @field_validator("name") + @classmethod + def validate_name(cls, value): + if value is None: + return value + + valid_role = ["administrator", "data-validator", "data-manager", "data-observer"] + if value not in valid_role: + raise UnprocessableEntity( + f"Role name must be one of the following: administrator, {', '.join(valid_role)}" + ) + + return value diff --git a/app/schemas/token_schema.py b/app/schemas/token_schema.py new file mode 100644 index 0000000..37290f2 --- /dev/null +++ b/app/schemas/token_schema.py @@ -0,0 +1,21 @@ +from datetime import datetime +from typing import Optional + +from .base import BaseSchema + + +class Token(BaseSchema): + access_token: str + refresh_token: str + expires_at: float + token_type: str = "bearer" + + +class TokenPayload(BaseSchema): + sub: Optional[str] = None + exp: Optional[datetime] = None + type: Optional[str] = None + + +class RefreshTokenSchema(BaseSchema): + refresh_token: str diff --git a/app/schemas/user_schema.py b/app/schemas/user_schema.py new file mode 100644 index 0000000..06868da --- /dev/null +++ b/app/schemas/user_schema.py @@ -0,0 +1,138 @@ +import re +from typing import Optional + +from pydantic import EmailStr, Field, field_validator + +from app.core.data_types import UUID7Field +from app.core.exceptions import UnprocessableEntity + +from .base import BaseSchema +from .organization_schema import OrganizationWithMapsetSchema +from .role_schema import RoleSchema + + +class UserSchema(BaseSchema): + id: UUID7Field + name: str + email: EmailStr + profile_picture: Optional[str] = None + username: str + position: Optional[str] = None + role: RoleSchema | None + employee_id: Optional[str] = None + organization: OrganizationWithMapsetSchema + is_active: bool = True + + +class UserCreateSchema(BaseSchema): + name: str = Field(..., min_length=4, max_length=100) + email: EmailStr + profile_picture: Optional[str] = Field(None) + username: str = Field(None, min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_]+$") + password: str = Field(..., min_length=8, max_length=128) + position: Optional[str] = Field(None) + role_id: UUID7Field + employee_id: Optional[str] = None + organization_id: UUID7Field + is_active: bool = True + + @field_validator("password") + @classmethod + def validate_password(cls, value): + if value is None: + return value + + has_letter = any(c.isalpha() for c in value) + has_digit = any(c.isdigit() for c in value) + has_special = any(c in "@$!%*#?&" for c in value) + + if not (has_letter and has_digit and has_special): + raise UnprocessableEntity( + "Password must be at least 8 characters long and contain at least one letter, one number, and one special character" + ) + + return value + + @field_validator("username") + @classmethod + def validate_username(cls, value): + if value is None: + return value + + if not re.match(r"^[a-zA-Z0-9_]+$", value): + raise UnprocessableEntity("Username can only contain letters, numbers, and underscores") + + return value + + @field_validator("email") + @classmethod + def validate_email_domain(cls, value): + if value is None: + return value + + # Validasi tambahan untuk domain email jika diperlukan + value.split("@")[1] + valid_domains = ["gmail.com", "yahoo.com", "hotmail.com", "company.com"] # Sesuaikan dengan kebutuhan + + # Hapus validasi ini jika tidak diperlukan atau sesuaikan dengan kebutuhan + # if domain not in valid_domains: + # raise UnprocessableEntity(f'Domain email tidak valid. Domain yang diizinkan: {", ".join(valid_domains)}') + + return value + + +class UserUpdateSchema(BaseSchema): + name: Optional[str] = Field(None, min_length=2, max_length=100) + email: Optional[EmailStr] = None + profile_picture: Optional[str] = Field(None, max_length=255) + username: Optional[str] = Field(None, min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_]+$") + password: Optional[str] = Field(None, min_length=8, max_length=128) + position: Optional[str] = Field(None) + role_id: Optional[UUID7Field] = Field(None) + employee_id: Optional[str] = Field(None, max_length=50) + organization_id: Optional[UUID7Field] = Field(None) + is_active: Optional[bool] = None + + @field_validator("password") + @classmethod + def validate_password(cls, value): + if value is None: + return value + + has_letter = any(c.isalpha() for c in value) + has_digit = any(c.isdigit() for c in value) + has_special = any(c in "@$!%*#?&" for c in value) + + if not (has_letter and has_digit and has_special): + raise UnprocessableEntity( + "Password must be at least 8 characters long and contain at least one letter, one number, and one special character" + ) + + return value + + @field_validator("username") + @classmethod + def validate_username(cls, value): + if value is None: + return value + + if not re.match(r"^[a-zA-Z0-9_]+$", value): + raise UnprocessableEntity("Username can only contain letters, numbers, and underscores") + + return value + + @field_validator("email") + @classmethod + def validate_email_domain(cls, value): + if value is None: + return value + + # Validasi tambahan untuk domain email jika diperlukan + value.split("@")[1] + valid_domains = ["gmail.com", "yahoo.com", "hotmail.com", "company.com"] # Sesuaikan dengan kebutuhan + + # Hapus validasi ini jika tidak diperlukan atau sesuaikan dengan kebutuhan + # if domain not in valid_domains: + # raise UnprocessableEntity(f'Domain email tidak valid. Domain yang diizinkan: {", ".join(valid_domains)}') + + return value diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..1c32522 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,37 @@ +from .auth_service import AuthService +from .base import BaseService +from .category_service import CategoryService +from .classification_service import ClassificationService +from .credential_service import CredentialService +from .feedback_service import FeedbackService +from .file_service import FileService +from .map_projection_system_service import MapProjectionSystemService +from .map_source_service import MapSourceService +from .mapset_history_service import MapsetHistoryService +from .mapset_service import MapsetService +from .news_service import NewsService +from .organization_service import OrganizationService +from .regional_service import RegionalService +from .role_service import RoleService +from .user_service import UserService +from .count_service import CountService + +__all__ = [ + "BaseService", + "OrganizationService", + "RoleService", + "UserService", + "AuthService", + "NewsService", + "FileService", + "CredentialService", + "FeedbackService", + "MapSourceService", + "MapProjectionSystemService", + "CategoryService", + "ClassificationService", + "RegionalService", + "MapsetService", + "MapsetHistoryService", + "CountService", +] diff --git a/app/services/auth_service.py b/app/services/auth_service.py new file mode 100644 index 0000000..4b4804d --- /dev/null +++ b/app/services/auth_service.py @@ -0,0 +1,83 @@ +from datetime import datetime, timedelta +from typing import Dict, Optional + +from fastapi import HTTPException, status +from pytz import timezone +from uuid6 import UUID + +from app.core.config import settings +from app.core.security import ( + create_access_token, + create_refresh_token, + decode_token, + verify_password, +) +from app.models.user_model import UserModel +from app.repositories.token_repository import TokenRepository +from app.repositories.user_repository import UserRepository + + +class AuthService: + tz = timezone(settings.TIMEZONE) + + def __init__(self, user_repository: UserRepository, token_repository: TokenRepository): + self.user_repository = user_repository + self.token_repository = token_repository + + async def authenticate_user(self, username: str, password: str) -> Optional[UserModel]: + """Autentikasi user dengan username dan password.""" + user = await self.user_repository.find_by_username(username) + if not user: + return None + if not verify_password(password, user.password): + return None + return user + + async def create_tokens(self, user_id: UUID) -> Dict[str, str]: + """Buat access dan refresh token.""" + access_token = create_access_token(user_id) + refresh_token = create_refresh_token(user_id) + + now = datetime.now(timezone(settings.TIMEZONE)) + + refresh_expires_at = now + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + expire_time = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + access_expires_at = expire_time.timestamp() + + refresh_token_data = { + "user_id": user_id, + "token": refresh_token, + "expires_at": refresh_expires_at, + } + await self.token_repository.create(refresh_token_data) + + return {"access_token": access_token, "refresh_token": refresh_token, "expires_at": access_expires_at} + + async def refresh_token(self, refresh_token: str) -> Dict[str, str]: + """Refresh access token menggunakan refresh token.""" + try: + payload = decode_token(refresh_token) + + if payload.get("type") != "refresh": + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type") + + user_id = payload.get("sub") + if not user_id: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials") + + token_obj = await self.token_repository.find_valid_token(refresh_token, UUID(user_id)) + if not token_obj: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Refresh token is invalid or expired" + ) + + await self.token_repository.revoke_token(refresh_token) + + return await self.create_tokens(UUID(user_id)) + + except Exception: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate credentials") + + async def logout(self, refresh_token: str) -> bool: + """Logout user dengan merevoke refresh token.""" + return await self.token_repository.revoke_token(refresh_token) diff --git a/app/services/base.py b/app/services/base.py new file mode 100644 index 0000000..a772564 --- /dev/null +++ b/app/services/base.py @@ -0,0 +1,204 @@ +from functools import lru_cache +from typing import Any, Dict, Generic, List, Tuple, Type, TypeVar, Union + +from sqlalchemy import or_ +from uuid6 import UUID + +from app.core.database import Base +from app.core.exceptions import NotFoundException, UnprocessableEntity +from app.repositories import BaseRepository + +ModelType = TypeVar("ModelType", bound=Base) +RepositoryType = TypeVar("RepositoryType", bound=BaseRepository) + + +class BaseService(Generic[ModelType, RepositoryType]): + """Optimized base service dengan caching dan performance improvements.""" + + def __init__(self, model: Type[ModelType], repository: Type[RepositoryType]): + self.model_class = model + self.repository = repository + self._valid_columns = set(self.model_class.__table__.columns.keys()) + self._has_soft_delete = hasattr(self.model_class, "is_deleted") + + @lru_cache(maxsize=256) + def _parse_filter_item(self, filter_item: str) -> Tuple[str, str]: + """Cache parsing filter.""" + try: + col, value = filter_item.split("=", 1) + return col.strip(), value.strip() + except ValueError: + raise UnprocessableEntity(f"Invalid filter {filter_item} must be 'name=value'") + + @lru_cache(maxsize=256) + def _parse_sort_item(self, sort_item: str) -> Tuple[str, str]: + """Cache parsing sort.""" + try: + col, order = sort_item.split(":", 1) + return col.strip(), order.strip().lower() + except ValueError: + raise UnprocessableEntity(f"Invalid sort {sort_item}. Must be 'name:asc' or 'name:desc'") + + def _validate_column(self, col: str) -> None: + """Validate column dengan cache.""" + if col not in self._valid_columns: + raise UnprocessableEntity(f"Invalid column: {col}") + + def _convert_value(self, col: str, value: str) -> Any: + """Convert value ke tipe yang sesuai.""" + if col == "id": + try: + return UUID(value) + except ValueError: + raise UnprocessableEntity(f"Invalid UUID value: {value}") + + if isinstance(value, str) and value.lower() in {"true", "false", "t", "f"}: + return value.lower() in {"true", "t"} + + if value.isdigit(): + return int(value) + + try: + return float(value) + except ValueError: + return value + + def _build_filters(self, filters: Union[str, list[str]]) -> List: + """Build filters dengan optimization.""" + list_model_filters = [] + + if isinstance(filters, str): + filters = [filters] + + if self._has_soft_delete: + filters.append("is_deleted=false") + + for filter_item in filters: + if isinstance(filter_item, list): + or_conditions = [] + for values in filter_item: + col, value = self._parse_filter_item(values) + self._validate_column(col) + converted_value = self._convert_value(col, value) + + if isinstance(converted_value, bool): + or_conditions.append(getattr(self.model_class, col).is_(converted_value)) + else: + or_conditions.append(getattr(self.model_class, col) == converted_value) + + if or_conditions: + list_model_filters.append(or_(*or_conditions)) + else: + col, value = self._parse_filter_item(filter_item) + self._validate_column(col) + converted_value = self._convert_value(col, value) + + if isinstance(converted_value, bool): + list_model_filters.append(getattr(self.model_class, col).is_(converted_value)) + else: + list_model_filters.append(getattr(self.model_class, col) == converted_value) + + return list_model_filters + + def _build_sort(self, sort: Union[str, list[str]]) -> List: + """Build sort dengan optimization.""" + if not sort: + return [] + + list_sort = [] + + if isinstance(sort, str): + sort = [sort] + + for sort_item in sort: + col, order = self._parse_sort_item(sort_item) + self._validate_column(col) + + if order == "asc": + list_sort.append(getattr(self.model_class, col).asc()) + elif order == "desc": + list_sort.append(getattr(self.model_class, col).desc()) + else: + raise UnprocessableEntity(f"Invalid sort order '{order}' for {col}") + + return list_sort + + async def find_by_id(self, id: UUID, relationships: List[str] = None) -> ModelType: + """Find record by ID dengan optional eager loading.""" + record = await self.repository.find_by_id(id, relationships=relationships) + if not record: + raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.") + return record + + async def find_all( + self, + filters: Union[str, list[str]] = None, + sort: Union[str, list[str]] = None, + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + relationships: List[str] = None, + searchable_columns: List[str] = None, + ) -> Tuple[List[ModelType], int]: + """Optimized find_all.""" + + if group_by: + self._validate_column(group_by) + + list_model_filters = self._build_filters(filters or []) + list_sort = self._build_sort(sort or []) + + return await self.repository.find_all( + filters=list_model_filters, + sort=list_sort, + search=search, + group_by=group_by, + limit=limit, + offset=offset, + relationships=relationships, + searchable_columns=searchable_columns, + ) + + async def create(self, data: Dict[str, Any]) -> ModelType: + """Create new record.""" + return await self.repository.create(data) + + async def update(self, id: UUID, data: Dict[str, Any], refresh: bool = True) -> ModelType: + """Update existing record.""" + # Check existence first + if not await self.repository.exists(id): + raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.") + + updated = await self.repository.update(id, data, refresh=refresh) + if not updated: + raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.") + + return updated + + async def delete(self, id: UUID, permanent: bool = False) -> None: + """Delete record dengan soft delete support.""" + if not await self.repository.exists(id): + raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.") + + if self._has_soft_delete and not permanent: + delete_data = {"is_deleted": True} + if hasattr(self.model_class, "is_active"): + delete_data["is_active"] = False + await self.repository.update(id, delete_data, refresh=False) + else: + await self.repository.delete(id) + + # Bulk operations + async def bulk_create(self, data_list: List[Dict[str, Any]], batch_size: int = 1000) -> List[ModelType]: + """Bulk create dengan validation.""" + return await self.repository.bulk_create(data_list, batch_size=batch_size, return_records=True) + + async def exists_by_id(self, id: UUID) -> bool: + """Check existence tanpa fetch object.""" + return await self.repository.exists(id) + + async def count_by_filters(self, filters: Union[str, list[str]] = None) -> int: + """Count records dengan filters.""" + list_model_filters = self._build_filters(filters or []) + return await self.repository.count(list_model_filters) diff --git a/app/services/category_service.py b/app/services/category_service.py new file mode 100644 index 0000000..35a1ed0 --- /dev/null +++ b/app/services/category_service.py @@ -0,0 +1,9 @@ +from app.models import CategoryModel +from app.repositories import CategoryRepository + +from . import BaseService + + +class CategoryService(BaseService[CategoryModel, CategoryRepository]): + def __init__(self, repository: CategoryRepository): + super().__init__(CategoryModel, repository) diff --git a/app/services/classification_service.py b/app/services/classification_service.py new file mode 100644 index 0000000..aba60c0 --- /dev/null +++ b/app/services/classification_service.py @@ -0,0 +1,9 @@ +from app.models import ClassificationModel +from app.repositories import ClassificationRepository + +from . import BaseService + + +class ClassificationService(BaseService[ClassificationModel, ClassificationRepository]): + def __init__(self, repository: ClassificationRepository): + super().__init__(ClassificationModel, repository) diff --git a/app/services/count_service.py b/app/services/count_service.py new file mode 100644 index 0000000..b898655 --- /dev/null +++ b/app/services/count_service.py @@ -0,0 +1,119 @@ +import httpx +from fastapi_async_sqlalchemy import db +from sqlalchemy import and_, func, select +from datetime import datetime + +from app.models import MapsetModel, OrganizationModel +from app.core.config import settings + + +class CountService: + GEONETWORK_API_URL = settings.GEONETWORK_API_URL + + async def _get_metadata_count(self) -> int: + """ + Fetch metadata count from GeoNetwork API. + Returns the total count from hits.total.value + """ + try: + timeout = httpx.Timeout(30.0) + request_body = { + "size": 0, + "track_total_hits": True, + "query": { + "bool": { + "must": { + "query_string": { + "query": "+isTemplate:n" + } + } + } + }, + "aggs": { + "resourceType": { + "terms": { + "field": "resourceType", + "size": 10 + } + } + } + } + + async with httpx.AsyncClient(timeout=timeout) as client: + response = await client.post( + self.GEONETWORK_API_URL, + json=request_body, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + response.raise_for_status() + data = response.json() + return data.get("hits", {}).get("total", {}).get("value", 0) + except Exception: + # Return 0 if API call fails + return 0 + + async def get_counts(self) -> dict: + """ + Return total counts for mapsets, organizations, and metadata from database and external API. + Visitor count is set to 0 for now. + """ + # Count mapsets with filters: is_deleted=False, is_active=True, status_validation='approved' + mapset_query = select(func.count()).select_from(MapsetModel).where( + and_( + MapsetModel.is_deleted.is_(False), + MapsetModel.is_active.is_(True), + MapsetModel.status_validation == "approved", + ) + ) + + # Sum download_count from mapsets with the same filters + download_count_query = select(func.sum(MapsetModel.download_count)).select_from(MapsetModel).where( + and_( + MapsetModel.is_deleted.is_(False), + MapsetModel.is_active.is_(True), + MapsetModel.status_validation == "approved", + ) + ) + + # Sum view_count from mapsets with the same filters + visitor_count_query = select(func.sum(MapsetModel.view_count)).select_from(MapsetModel).where( + and_( + MapsetModel.is_deleted.is_(False), + MapsetModel.is_active.is_(True), + MapsetModel.status_validation == "approved", + ) + ) + + # Count organizations that have mapsets + # Only count organizations that have at least one mapset with the specified filters + organization_query = ( + select(func.count(func.distinct(OrganizationModel.id))) + .select_from(OrganizationModel) + .join(MapsetModel, OrganizationModel.id == MapsetModel.producer_id) + .where( + and_( + OrganizationModel.is_deleted.is_(False), + OrganizationModel.is_active.is_(True), + MapsetModel.is_deleted.is_(False), + MapsetModel.is_active.is_(True), + MapsetModel.status_validation == "approved", + ) + ) + ) + + mapset_count = await db.session.scalar(mapset_query) or 0 + download_count = await db.session.scalar(download_count_query) or 0 + organization_count = await db.session.scalar(organization_query) or 0 + metadata_count = await self._get_metadata_count() + visitor_count = await db.session.scalar(visitor_count_query) or 0 + + return { + "mapset_count": int(mapset_count), + "organization_count": int(organization_count), + "visitor_count": visitor_count, + "metadata_count": int(metadata_count), + "download_count": int(download_count), + } diff --git a/app/services/credential_service.py b/app/services/credential_service.py new file mode 100644 index 0000000..fc72dc4 --- /dev/null +++ b/app/services/credential_service.py @@ -0,0 +1,303 @@ +from typing import Any, Dict, List, Optional, Tuple +from uuid import UUID + +from fastapi import HTTPException, status +from pydantic import ValidationError + +from app.core.exceptions import NotFoundException +from app.models.credential_model import CredentialModel +from app.repositories.credential_repository import CredentialRepository +from app.schemas.user_schema import UserSchema +from app.utils.encryption import credential_encryption + +from . import BaseService + + +class CredentialService(BaseService[CredentialModel, CredentialRepository]): + def __init__(self, repository: CredentialRepository): + super().__init__(CredentialModel, repository) + + async def create_credential( + self, + name: str, + credential_type: str, + sensitive_data: Dict[str, Any], + credential_metadata: Optional[Dict[str, Any]] = None, + description: Optional[str] = None, + is_default: bool = False, + user_id: UUID = None, + ) -> CredentialModel: + """ + Buat kredensial baru dengan mengenkripsi data sensitif. + + Args: + name: Nama kredensial + credential_type: Tipe kredensial ('database', 'api', 'minio', dll) + sensitive_data: Data sensitif yang akan dienkripsi + credential_metadata: metadata tidak sensitif (opsional) + description: Deskripsi kredensial (opsional) + is_default: Apakah kredensial ini default untuk tipenya + user_id: ID user yang membuat kredensial + + Returns: + Credential model yang telah disimpan + """ + + encrypted_data, encryption_iv = credential_encryption.encrypt(sensitive_data) + + credential_data = { + "name": name, + "description": description, + "credential_type": credential_type, + "encrypted_data": encrypted_data, + "encryption_iv": encryption_iv, + "credential_metadata": credential_metadata or {}, + "is_default": is_default, + "created_by": user_id, + "updated_by": user_id, + } + + return await self.repository.create(credential_data) + + async def get_credential_with_decrypted_data(self, credential_id: UUID) -> Dict[str, Any]: + """ + Ambil kredensial beserta data sensitif yang sudah didekripsi. + + Args: + credential_id: ID kredensial + + Returns: + Tuple dari (credential_model, decrypted_data) + """ + credential = await self.find_by_id(credential_id) + + if not credential: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Credential not found") + + decrypted_data = credential_encryption.decrypt(credential.encrypted_data, credential.encryption_iv) + + credential_dict = credential.to_dict() + credential_dict["decrypted_data"] = decrypted_data + return credential_dict + + async def get_list_of_decrypted_credentials( + self, filters: list, sort: list = [], search: str = "", group_by: str = None, limit: int = 100, offset: int = 0 + ) -> Tuple[List[Dict[str, Any]], int]: + """ + Ambil kredensial beserta data sensitif yang sudah didekripsi. + + Args: + credential_id: ID kredensial + + Returns: + Tuple dari (credential_model, decrypted_data) + """ + + credentials, total = await self.find_all( + filters=filters, sort=sort, search=search, group_by=group_by, limit=limit, offset=offset + ) + decrypted_credentials = [] + for credential in credentials: + try: + decrypted_data = credential_encryption.decrypt(credential.encrypted_data, credential.encryption_iv) + temp = credential.to_dict() + temp["decrypted_data"] = decrypted_data + decrypted_credentials.append(temp) + except ValidationError: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to decrypt some credential data" + ) + + return decrypted_credentials, total + + async def update_credential(self, credential_id: UUID, data: Dict[str, Any], user_id: UUID) -> CredentialModel: + """ + Update kredensial. + + Args: + credential_id: ID kredensial yang akan diupdate + data: Data yang akan diupdate (dapat berisi 'sensitive_data', 'credential_metadata', dll) + user_id: ID user yang melakukan update + + Returns: + Updated credential model + """ + credential = await self.find_by_id(credential_id) + + if not credential: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Credential not found") + if "credential_metadata" in data: + if not isinstance(data["credential_metadata"], dict): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="credential_metadata must be a dictionary" + ) + metadata = credential.credential_metadata + metadata.update(data["credential_metadata"]) + data["credential_metadata"] = metadata + + for key, value in data.items(): + if key != "sensitive_data" and hasattr(credential, key): + setattr(credential, key, value) + + if "sensitive_data" in data and data["sensitive_data"]: + current_data = credential_encryption.decrypt(credential.encrypted_data, credential.encryption_iv) + + current_data.update(data["sensitive_data"]) + encrypted_data, encryption_iv = credential_encryption.encrypt(current_data) + credential.encrypted_data = encrypted_data + credential.encryption_iv = encryption_iv + + credential.updated_by = user_id + + if data.get("is_default", False) and not credential.is_default: + await self.repository.set_default(credential_id, user_id) + + return await self.repository.update(credential_id, credential.to_dict()) + + async def get_credentials_by_type(self, credential_type: str, is_active: bool = True) -> List[CredentialModel]: + """ + Dapatkan semua kredensial berdasarkan tipe. + + Args: + credential_type: Tipe kredensial + is_active: Filter berdasarkan status aktif + + Returns: + List dari credential models + """ + return await self.repository.get_by_type(credential_type, is_active) + + async def get_default_credential( + self, credential_type: str, with_decrypted_data: bool = False + ) -> Tuple[Optional[CredentialModel], Optional[Dict[str, Any]]]: + """ + Dapatkan kredensial default berdasarkan tipe. + + Args: + db: Database session + credential_type: Tipe kredensial + with_decrypted_data: Apakah akan mendekripsi data sensitif + + Returns: + Tuple dari (credential_model, decrypted_data) + """ + credential = await self.repository.get_default_by_type(credential_type, is_active=True) + + if not credential: + return None, None + + if with_decrypted_data: + try: + decrypted_data = credential_encryption.decrypt(credential.encrypted_data, credential.encryption_iv) + return credential, decrypted_data + except Exception: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to decrypt credential data" + ) + + return credential, None + + async def delete(self, user: UserSchema, id: UUID) -> None: + credential = await self.find_by_id(id) + if not credential: + raise NotFoundException(f"Credential with ID {id} not found") + + delete_by_dict = {"is_deleted": True, "is_active": False, "updated_by": user.id} + + await self.repository.update(id, delete_by_dict) + + async def test_credential(self, credential_id: UUID, user_id: UUID) -> Dict[str, Any]: + """ + Test koneksi menggunakan kredensial. + Implementasi akan berbeda tergantung tipe kredensial. + + Args: + db: Database session + credential_id: ID kredensial yang akan ditest + user_id: ID user yang melakukan test + + Returns: + Dictionary berisi hasil test + """ + # Dapatkan kredensial dengan data terdekripsi + credential, decrypted_data = await self.get_credential_with_decrypted_data(credential_id) + + result = {"success": False, "details": {}} + + try: + # Lakukan test berdasarkan tipe kredensial + if credential.credential_type == "database": + # Implementasi test untuk database + result = await self._test_database_credential(decrypted_data) + elif credential.credential_type == "minio": + # Implementasi test untuk MinIO + result = await self._test_minio_credential(decrypted_data) + elif credential.credential_type == "api": + # Implementasi test untuk API + result = await self._test_api_credential(decrypted_data) + else: + result = { + "success": False, + "details": {"message": f"Testing for type {credential.credential_type} not implemented"}, + } + + # Update timestamp penggunaan terakhir + await self.repository.update_last_used(credential_id, user_id) + + except Exception as e: + result = {"success": False, "details": {"error": str(e)}} + + return result + + async def _test_database_credential(self, cred_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Test koneksi database. + + Args: + cred_data: Data kredensial yang sudah didekripsi + + Returns: + Dictionary berisi hasil test + """ + # Implementasi test koneksi database + # Contoh: + try: + # Simulasi test + # Dalam implementasi sebenarnya, lakukan koneksi ke database + return {"success": True, "details": {"message": "Database connection successful"}} + except Exception as e: + return {"success": False, "details": {"error": str(e)}} + + async def _test_minio_credential(self, cred_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Test koneksi MinIO. + + Args: + cred_data: Data kredensial yang sudah didekripsi + + Returns: + Dictionary berisi hasil test + """ + try: + # Implementasi test koneksi MinIO + # Di sini bisa menggunakan miniopy-async untuk tes koneksi + return {"success": True, "details": {"message": "MinIO connection successful"}} + except Exception as e: + return {"success": False, "details": {"error": str(e)}} + + async def _test_api_credential(self, cred_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Test koneksi API. + + Args: + cred_data: Data kredensial yang sudah didekripsi + + Returns: + Dictionary berisi hasil test + """ + try: + # Implementasi test koneksi API + # Di sini bisa menggunakan aiohttp untuk tes koneksi + return {"success": True, "details": {"message": "API connection successful"}} + except Exception as e: + return {"success": False, "details": {"error": str(e)}} diff --git a/app/services/feedback_service.py b/app/services/feedback_service.py new file mode 100644 index 0000000..1aedaf0 --- /dev/null +++ b/app/services/feedback_service.py @@ -0,0 +1,8 @@ +from app.models.feedback_model import FeedbackModel +from app.repositories import FeedbackRepository +from app.services.base import BaseService + + +class FeedbackService(BaseService[FeedbackModel, FeedbackRepository]): + def __init__(self, repository: FeedbackRepository): + super().__init__(FeedbackModel, repository) diff --git a/app/services/file_service.py b/app/services/file_service.py new file mode 100644 index 0000000..d1e2b14 --- /dev/null +++ b/app/services/file_service.py @@ -0,0 +1,164 @@ +import io +import os +from datetime import datetime +from typing import Any, BinaryIO, Dict, List, Tuple +from uuid import UUID + +from fastapi import HTTPException, UploadFile, status +from pytz import timezone + +from app.core.config import settings +from app.core.minio_client import MinioClient +from app.models.file_model import FileModel +from app.repositories.file_repository import FileRepository + +from . import BaseService + + +class FileService(BaseService[FileModel, FileRepository]): + def __init__(self, repository: FileRepository, minio_client: MinioClient): + super().__init__(FileModel, repository) + + self.minio_client = minio_client + + async def validate_file_extension(self, filename: str) -> bool: + """Validasi ekstensi file.""" + ext = os.path.splitext(filename)[1][1:].lower() + if ext not in settings.ALLOWED_EXTENSIONS: + allowed_exts = ", ".join(settings.ALLOWED_EXTENSIONS) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Ekstensi file tidak diperbolehkan. Ekstensi yang diperbolehkan: {allowed_exts}", + ) + return True + + async def validate_file_size(self, file_size: int) -> bool: + """Cek ukuran file.""" + if file_size > settings.MAX_UPLOAD_SIZE: + max_size_mb = settings.MAX_UPLOAD_SIZE / (1024 * 1024) + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"File terlalu besar. Ukuran maksimum adalah {max_size_mb:.1f}MB", + ) + return True + + async def upload_file(self, file: UploadFile, description: str = None, user_id: str = None) -> FileModel: + """ + Upload file ke MinIO dan simpan metadata ke database. + + Args: + file: File yang diupload + description: Deskripsi file (opsional) + user_id: ID pengguna yang mengupload + + Returns: + Model file yang telah disimpan + """ + try: + await self.validate_file_extension(file.filename) + + object_name = f"{datetime.now(timezone(settings.TIMEZONE)).strftime('%Y%m%d%S')}-{file.filename}" + + content = await file.read() + content_length = len(content) + + await self.validate_file_size(content_length) + + file_data = io.BytesIO(content) + + metadata = {"filename": file.filename, "description": description or "", "uploaded_by": str(user_id)} + + url = await self.minio_client.upload_file( + file_data=file_data, + object_name=object_name, + content_type=file.content_type, + content_length=content_length, + metadata=metadata, + ) + + file_data = { + "filename": file.filename, + "object_name": object_name, + "content_type": file.content_type, + "size": content_length, + "description": description, + "url": url, + "user_id": user_id, + } + + db_file = await self.create(file_data) + return db_file + + except HTTPException as e: + raise e + except Exception: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Gagal mengupload file") + + async def get_file_content(self, file_id: UUID) -> Tuple[BinaryIO, Dict[str, Any], FileModel]: + """ + Ambil konten file dari MinIO. + + Args: + file_id: ID file di database + + Returns: + Tuple dari (file content, object info, file model) + """ + try: + file_model = await self.find_by_id(file_id) + if not file_model: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File tidak ditemukan") + + object_content, object_info = await self.minio_client.get_file(file_model.object_name) + + return object_content, object_info, file_model + + except HTTPException as e: + raise e + + async def delete_file_with_content(self, file_id: str, user_id: str) -> bool: + """ + Hapus file dari MinIO dan database. + + Args: + file_id: ID file di database + user_id: ID pengguna yang ingin menghapus file + + Returns: + Boolean yang menunjukkan keberhasilan operasi + """ + try: + file_model = await self.find_by_id(file_id) + if not file_model: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File tidak ditemukan") + + if str(file_model.uploaded_by) != user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Anda tidak memiliki akses untuk menghapus file ini" + ) + + await self.minio_client.delete_file(file_model.object_name) + + await self.delete(file_id) + + return True + + except HTTPException as e: + raise e + except Exception: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Gagal menghapus file") + + async def get_files_by_user(self, user_id: str, limit: int = 100, offset: int = 0) -> Tuple[List[FileModel], int]: + """ + Ambil daftar file yang diupload oleh user tertentu. + + Args: + user_id: ID pengguna + limit: Jumlah maksimum hasil yang dikembalikan + offset: Offset untuk paginasi + + Returns: + Tuple dari (list file, total_count) + """ + filter_params = {"uploaded_by": user_id} + return await self.find_all(filter=filter_params, limit=limit, offset=offset) diff --git a/app/services/map_access_service.py b/app/services/map_access_service.py new file mode 100644 index 0000000..cc94b69 --- /dev/null +++ b/app/services/map_access_service.py @@ -0,0 +1,24 @@ +from app.models import MapAccessModel +from app.repositories import MapAccessRepository + +from . import BaseService + + +class MapAccessService(BaseService[MapAccessModel, MapAccessRepository]): + def __init__(self, repository: MapAccessRepository): + super().__init__(MapAccessModel, repository) + + async def find_by_mapset(self, mapset_id: str): + return await self.repository.find_by_mapset(mapset_id) + + async def find_by_user(self, user_id: str): + return await self.repository.find_by_user(user_id) + + async def find_by_organization(self, organization_id: str): + return await self.repository.find_by_organization(organization_id) + + async def find_user_access_to_mapset(self, mapset_id: str, user_id: str): + return await self.repository.find_user_access_to_mapset(mapset_id, user_id) + + async def find_organization_access_to_mapset(self, mapset_id: str, organization_id: str): + return await self.repository.find_organization_access_to_mapset(mapset_id, organization_id) diff --git a/app/services/map_projection_system_service.py b/app/services/map_projection_system_service.py new file mode 100644 index 0000000..9335b42 --- /dev/null +++ b/app/services/map_projection_system_service.py @@ -0,0 +1,9 @@ +from app.models import MapProjectionSystemModel +from app.repositories import MapProjectionSystemRepository + +from . import BaseService + + +class MapProjectionSystemService(BaseService[MapProjectionSystemModel, MapProjectionSystemRepository]): + def __init__(self, repository: MapProjectionSystemRepository): + super().__init__(MapProjectionSystemModel, repository) diff --git a/app/services/map_source_service.py b/app/services/map_source_service.py new file mode 100644 index 0000000..3360fbd --- /dev/null +++ b/app/services/map_source_service.py @@ -0,0 +1,9 @@ +from app.models import MapSourceModel +from app.repositories import MapSourceRepository + +from . import BaseService + + +class MapSourceService(BaseService[MapSourceModel, MapSourceRepository]): + def __init__(self, repository: MapSourceRepository): + super().__init__(MapSourceModel, repository) diff --git a/app/services/mapset_history_service.py b/app/services/mapset_history_service.py new file mode 100644 index 0000000..cd4dbb7 --- /dev/null +++ b/app/services/mapset_history_service.py @@ -0,0 +1,16 @@ +from typing import Any, Dict + +from app.models import MapsetHistoryModel +from app.repositories import MapsetHistoryRepository +from app.schemas.user_schema import UserSchema + +from . import BaseService + + +class MapsetHistoryService(BaseService[MapsetHistoryModel, MapsetHistoryRepository]): + def __init__(self, repository: MapsetHistoryRepository): + super().__init__(MapsetHistoryModel, repository) + + async def create(self, user: UserSchema, data: Dict[str, Any]) -> MapsetHistoryModel: + data.update({"user_id": user.id}) + return await super().create(data) diff --git a/app/services/mapset_service.py b/app/services/mapset_service.py new file mode 100644 index 0000000..e5d368e --- /dev/null +++ b/app/services/mapset_service.py @@ -0,0 +1,424 @@ +import json +import math +from typing import Any, Dict, List, Optional, Tuple +from uuid import UUID + +import httpx +import numpy as np +from colour import Color +from fastapi import HTTPException, status +from shapely.geometry import Point, shape +from sqlalchemy import func, or_ + +from app.core.exceptions import UnprocessableEntity +from app.models import MapsetModel +from app.models.organization_model import OrganizationModel +from app.repositories import ( + MapsetHistoryRepository, + MapsetRepository, + SourceUsageRepository, +) +from app.schemas.user_schema import UserSchema +from app.services.file_service import FileService +from app.core.exceptions import NotFoundException, UnprocessableEntity + +from . import BaseService + + +class MapsetService(BaseService[MapsetModel, MapsetRepository]): + def __init__( + self, + repository: MapsetRepository, + history_repository: MapsetHistoryRepository, + source_usage_repository: SourceUsageRepository, + file_service: FileService, + ): + super().__init__(MapsetModel, repository) + + self.history_repository = history_repository + self.source_usage_repository = source_usage_repository + self.file_service = file_service + + async def find_all( + self, + user: UserSchema, + filters: str | List[str], + sort: str | List[str], + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + landing: bool = False, + ) -> Tuple[List[MapsetModel] | int]: + if group_by: + self._validate_column(group_by) + + list_model_filters = self._build_filters(filters or []) + list_sort = self._build_sort(sort or []) + + return await self.repository.find_all(user, list_model_filters, list_sort, search, group_by, limit, offset, landing) + + async def find_by_id(self, id: UUID, relationships: List[str] = None, user: UserSchema = None) -> MapsetModel: + """Find record by ID dengan optional eager loading.""" + record = await self.repository.find_by_id(id, relationships=relationships) + if not record: + raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.") + + if user is None: + await self.repository.increment_view_count(id) + record = await self.repository.find_by_id(id, relationships=relationships) + + return record + + async def find_all_group_by_organization( + self, + user: Optional[UserSchema] = None, + filters: Optional[list[str]] = None, + sort: Optional[list[str]] = None, + search: str = "", + limit: int = 100, + offset: int = 0, + ) -> Tuple[List[Dict], int]: + """ + Find organizations with filtered mapsets. + Only returns the mapsets that match the filter for each organization. + """ + mapset_filters = [] + organization_filters = [] + list_sort = [] + + filters = filters or [] + + if isinstance(filters, str): + filters = [filters] + + filters.append("is_deleted=false") + + for filter_str in filters: + if isinstance(filter_str, list): + mapset_or_conditions = [] + org_or_conditions = [] + + for value_str in filter_str: + col, value = value_str.split("=") + if hasattr(MapsetModel, col): + if value.lower() in {"true", "false", "t", "f"}: + value = value.lower() in {"true", "t"} + mapset_or_conditions.append(getattr(MapsetModel, col) == value) + elif hasattr(OrganizationModel, col): + if value.lower() in {"true", "false", "t", "f"}: + value = value.lower() in {"true", "t"} + org_or_conditions.append(getattr(OrganizationModel, col) == value) + else: + raise UnprocessableEntity(f"Invalid filter column: {col}") + + if mapset_or_conditions: + mapset_filters.append(or_(*mapset_or_conditions)) + if org_or_conditions: + organization_filters.append(or_(*org_or_conditions)) + continue + + try: + col, value = filter_str.split("=") + + # Konversi nilai boolean jika perlu + if value.lower() in {"true", "false", "t", "f"}: + value = value.lower() in {"true", "t"} + + # Tambahkan filter ke daftar yang sesuai + if hasattr(MapsetModel, col): + mapset_filters.append(getattr(MapsetModel, col) == value) + elif hasattr(OrganizationModel, col): + organization_filters.append(getattr(OrganizationModel, col) == value) + else: + raise UnprocessableEntity(f"Invalid filter column: {col}") + except ValueError: + raise UnprocessableEntity(f"Invalid filter format: {filter_str}") + + if isinstance(sort, str): + sort = [sort] + + for sort_str in sort or []: + try: + col, order = sort_str.split(":") + + if hasattr(OrganizationModel, col): + sort_col = getattr(OrganizationModel, col) + elif hasattr(MapsetModel, col): + # Untuk sort berdasarkan atribut mapset, kita perlu subquery + # Ini tidak diimplementasi di sini untuk menjaga kesederhanaan + # Namun Anda bisa mengembangkannya jika diperlukan + # continue + sort_col = getattr(MapsetModel, col) + else: + raise UnprocessableEntity(f"Invalid sort column: {col}") + + if order.lower() == "asc": + list_sort.append(sort_col.asc()) + elif order.lower() == "desc": + list_sort.append(sort_col.desc()) + else: + raise UnprocessableEntity(f"Invalid sort order: {order}") + except ValueError: + raise UnprocessableEntity(f"Invalid sort format: {sort_str}") + + if not list_sort: + list_sort = [OrganizationModel.name.asc()] + + return await self.repository.find_all_group_by_organization( + user=user, + mapset_filters=mapset_filters, + organization_filters=organization_filters, + sort=list_sort, + search=search, + limit=limit, + offset=offset, + ) + + async def create(self, user: UserSchema, data: Dict[str, Any]) -> MapsetModel: + data["created_by"] = user.id + data["updated_by"] = user.id + data["created_at"] = func.timezone('Asia/Jakarta', func.now()) + data["updated_at"] = func.timezone('Asia/Jakarta', func.now()) + + track_note = data.pop("notes", None) + source_id = data.pop("source_id", None) + + mapset = await super().create(data) + + if source_id: + list_source_usage = [] + if isinstance(source_id, str) or isinstance(source_id, UUID): + source_id = [source_id] + + for id in source_id: + list_source_usage.append({"mapset_id": mapset.id, "source_id": id}) + + await self.source_usage_repository.bulk_create(list_source_usage) + + await self.history_repository.create( + { + "mapset_id": mapset.id, + "validation_type": mapset.status_validation, + "notes": track_note, + "user_id": user.id, + "timestamp": func.timezone('Asia/Jakarta', func.now()), + } + ) + + return mapset + + async def update(self, id: UUID, user: UserSchema, data: Dict[str, Any]) -> MapsetModel: + data["updated_by"] = user.id + data["updated_at"] = func.timezone('Asia/Jakarta', func.now()) + track_note = data.pop("notes", None) + source_id = data.pop("source_id", None) + + mapset = await super().update(id, data) + + if source_id: + list_source_usage = [] + if isinstance(source_id, str) or isinstance(source_id, UUID): + source_id = [source_id] + + for id in source_id: + list_source_usage.append({"mapset_id": mapset.id, "source_id": id}) + + await self.source_usage_repository.bulk_update(mapset.id, list_source_usage) + + await self.history_repository.create( + { + "mapset_id": mapset.id, + "validation_type": mapset.status_validation, + "notes": track_note, + "user_id": user.id, + "timestamp": func.timezone('Asia/Jakarta', func.now()), + } + ) + + return mapset + + async def bulk_update_activation(self, mapset_ids: List[UUID], is_active: bool) -> None: + await self.repository.bulk_update_activation(mapset_ids, is_active) + + async def increment_download_count(self, id: UUID) -> None: + """Increment download_count for a mapset by 1.""" + # Ensure mapset exists before incrementing + record = await self.repository.find_by_id(id) + if not record: + raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.") + await self.repository.increment_download_count(id) + + async def calculate_choropleth( + self, geojson_data: Dict, boundary_file_id: UUID, coordinate_field: str = "coordinates" + ) -> List[Dict]: + """ + Menghitung data choropleth berdasarkan jumlah titik dalam poligon. + + Args: + geojson_data: GeoJSON data yang berisi titik-titik + boundary_name: Nama file boundary GeoJSON di dalam folder assets + coordinate_field: Nama field yang berisi koordinat di dalam geometri + + Returns: + List[Dict]: Data choropleth untuk setiap poligon dengan jumlah titik + """ + if not geojson_data or not isinstance(geojson_data, dict) or "features" not in geojson_data: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid GeoJSON data format") + + features = geojson_data.get("features", []) + + boundary_geojson = await self._load_boundary_geojson(boundary_file_id) + polygon_features = boundary_geojson.get("features", []) + + choropleth_data = [] + + for polygon in polygon_features: + value = 0 + polygon_shape = shape(polygon["geometry"]) + + for feature in features: + if feature.get("geometry", {}).get("type") == "Point" and coordinate_field in feature.get( + "geometry", {} + ): + coords = feature["geometry"]["coordinates"] + if len(coords) >= 2: + point = Point(coords[0], coords[1]) + + if polygon_shape.contains(point): + value += 1 + + choropleth_data.append({**polygon["properties"], "value": value}) + + return choropleth_data + + async def generate_colorscale( + self, geojson_source: str, color_range: List[str] = None, boundary_file_id: UUID = None + ) -> Tuple[List[Dict], List[Dict]]: + """ + Generate color scale untuk data choropleth. + + Args: + geojson_source: geojson_source url menuju data choropleth + color_range: Rentang warna yang akan digunakan + + Returns: + Tuple[List[Dict], List[Dict]]: + - Data choropleth dengan warna + - Color scale untuk legenda + """ + timeout = httpx.Timeout(60.0, connect=10.0) + async with httpx.AsyncClient(verify=False, timeout=timeout) as client: + try: + response = await client.get(geojson_source) + response.raise_for_status() + except httpx.TimeoutException: + raise HTTPException( + status_code=status.HTTP_408_REQUEST_TIMEOUT, detail="Request timed out while fetching geojson data" + ) + except httpx.HTTPStatusError: + raise HTTPException( + status_code=400, detail="Failed to fetch geojson, please provide valid geojson url" + ) + + try: + geojson_data = response.json() + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid Source URL, please provide JSON/GEOJSON return response", + ) + choropleth_data = await self.calculate_choropleth(geojson_data, boundary_file_id) + if not color_range: + color_range = ["#ddffed", "#006430"] + + arr_tobe_percentile = [] + for row in choropleth_data: + arr_tobe_percentile.append(row["value"]) + + arr_tobe_percentile.sort() + arr_tobe_percentile = list(filter(lambda num: num != 0, arr_tobe_percentile)) + + count = 5 + is_duplicate = False + arr_percentile = [] + + if len(arr_tobe_percentile) > 1: + while count >= 1: + diff = 100 / count + arr_percentile = [] + + for j in range(count + 1): + perc = math.ceil(np.percentile(arr_tobe_percentile, j * diff)) + arr_percentile.append(perc) + + is_duplicate = len(arr_percentile) != len(set(arr_percentile)) + if is_duplicate: + count = count - 1 + arr_tobe_percentile = arr_percentile.copy() + else: + break + elif len(arr_tobe_percentile) == 1: + arr_percentile = arr_tobe_percentile + else: + pass + + if len(arr_percentile) > 1: + colors = list(Color(color_range[0]).range_to(Color(color_range[1]), len(arr_percentile) - 1)) + + rangelist = [] + rangelist.append({"from": 0, "to": 0, "color": "#FFFFFFFF", "total_cluster": 0}) + + for i in range(len(arr_percentile) - 1): + if i == 0: + rangelist.append( + { + "from": arr_percentile[i], + "to": arr_percentile[i + 1], + "color": colors[i].hex, + "total_cluster": 0, + } + ) + else: + rangelist.append( + { + "from": arr_percentile[i] + 1, + "to": arr_percentile[i + 1], + "color": colors[i].hex, + "total_cluster": 0, + } + ) + elif len(arr_percentile) == 1: + colors = list(Color(color_range[0]).range_to(Color(color_range[1]), 1)) + + rangelist = [] + rangelist.append({"from": 0, "to": 0, "color": "#FFFFFFFF", "total_cluster": 0}) + rangelist.append( + {"from": arr_percentile[0], "to": arr_percentile[0], "color": colors[0].hex, "total_cluster": 0} + ) + else: + rangelist = [] + rangelist.append({"from": 0, "to": 0, "color": "#FFFFFFFF", "total_cluster": 0}) + + result = [] + for item in choropleth_data: + temp = item.copy() + + for range_item in rangelist: + if temp["value"] >= range_item["from"] and temp["value"] <= range_item["to"]: + temp["color"] = range_item["color"] + range_item["total_cluster"] += 1 + + result.append(temp) + + return result, rangelist + + async def _load_boundary_geojson(self, boundary_file_id: UUID) -> Dict: + try: + object_content, _, _ = await self.file_service.get_file_content(boundary_file_id) + json_data = json.loads(await object_content.read()) + return json_data + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed read boundary: {str(e)}" + ) diff --git a/app/services/news_service.py b/app/services/news_service.py new file mode 100644 index 0000000..d47d171 --- /dev/null +++ b/app/services/news_service.py @@ -0,0 +1,15 @@ +from typing import List +from uuid import UUID + +from app.models import NewsModel +from app.repositories import NewsRepository + +from . import BaseService + + +class NewsService(BaseService[NewsModel, NewsRepository]): + def __init__(self, repository: NewsRepository): + super().__init__(NewsModel, repository) + + async def bulk_update_activation(self, news_ids: List[UUID], is_active: bool) -> None: + await self.repository.bulk_update_activation(news_ids, is_active) diff --git a/app/services/organization_service.py b/app/services/organization_service.py new file mode 100644 index 0000000..8b3e8e9 --- /dev/null +++ b/app/services/organization_service.py @@ -0,0 +1,78 @@ +from typing import Dict, Optional + +from fastapi import HTTPException, status +from uuid6 import UUID + +from app.core.exceptions import NotFoundException +from app.models.organization_model import OrganizationModel +from app.repositories.organization_repository import OrganizationRepository +from app.schemas.user_schema import UserSchema + +from . import BaseService + + +class OrganizationService(BaseService[OrganizationModel, OrganizationRepository]): + def __init__(self, repository: OrganizationRepository): + super().__init__(OrganizationModel, repository) + + async def get_organizations_by_id(self, user: UserSchema, id: UUID) -> Dict[str, str]: + try: + organization = await self.repository.find_by_id(user, id) + if organization is None: + raise NotFoundException(f"Organization with UUID {id} not found.") + + return organization + except HTTPException as e: + raise HTTPException(status_code=e.status_code, detail=e.detail) + + async def find_by_name(self, name: str, sensitive: bool = False) -> Optional[OrganizationModel]: + try: + return await self.repository.find_by_name(name, sensitive) + except HTTPException as e: + raise HTTPException(status_code=e.status_code, detail=e.detail) + + async def find_all(self, user: UserSchema | None, filters, sort, search="", group_by=None, limit=100, offset=0, landing=False): + if group_by: + self._validate_column(group_by) + + list_model_filters = self._build_filters(filters or []) + list_sort = self._build_sort(sort or []) + + return await self.repository.find_all( + user, + filters=list_model_filters, + sort=list_sort, + search=search, + group_by=group_by, + limit=limit, + offset=offset, + landing=landing, + ) + + async def create(self, data: Dict[str, str]) -> OrganizationModel: + # if await self.find_by_name(data["name"], True): + # raise HTTPException( + # status_code=status.HTTP_400_BAD_REQUEST, detail="Organization with this name already exists." + # ) + + return await super().create(data) + + async def update(self, id: UUID, data: Dict[str, str]) -> OrganizationModel: + # Pass None as user to get public view of organization + organization = await self.repository.find_by_id(None, id) + if not organization: + raise NotFoundException(f"Organization with UUID {id} not found.") + + if "name" in data and await self.find_by_name(data["name"]): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="Organization with this name already exists." + ) + + return await self.repository.update(id, data) + + async def delete(self, id: UUID) -> None: + organization = await self.repository.find_by_id(None, id) + if not organization: + raise NotFoundException(f"Organization with UUID {id} not found.") + + return await self.repository.delete(id) diff --git a/app/services/regional_service.py b/app/services/regional_service.py new file mode 100644 index 0000000..1d0dfdd --- /dev/null +++ b/app/services/regional_service.py @@ -0,0 +1,9 @@ +from app.models import RegionalModel +from app.repositories import RegionalRepository + +from . import BaseService + + +class RegionalService(BaseService[RegionalModel, RegionalRepository]): + def __init__(self, repository: RegionalRepository): + super().__init__(RegionalModel, repository) diff --git a/app/services/role_service.py b/app/services/role_service.py new file mode 100644 index 0000000..b65ea9b --- /dev/null +++ b/app/services/role_service.py @@ -0,0 +1,17 @@ +from app.core.exceptions import NotFoundException +from app.models import RoleModel +from app.repositories import RoleRepository + +from . import BaseService + + +class RoleService(BaseService[RoleModel, RoleRepository]): + def __init__(self, repository: RoleRepository): + super().__init__(RoleModel, repository) + + async def find_by_name(self, name: str) -> RoleModel: + """Find record by name.""" + record = await self.repository.find_by_name(name) + if not record: + raise NotFoundException(f"{self.model_class.__name__} with name {name} not found.") + return record diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..9906141 --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,150 @@ +from typing import Dict, List, Tuple, Union, override + +from fastapi import HTTPException, status +from uuid6 import UUID + +from app.core.exceptions import NotFoundException +from app.core.security import get_password_hash +from app.models import UserModel +from app.repositories import UserRepository +from app.repositories.role_repository import RoleRepository +from app.schemas.user_schema import UserSchema + +from . import BaseService + + +class UserService(BaseService[UserModel, UserRepository]): + access_control_level = { + "data_viewer": {"data_viewer"}, + "data_manager": {"data_manager", "data_viewer"}, + "data_validator": {"data_validator", "data_manager", "data_viewer"}, + "administrator": { + "administrator", + "data_validator", + "data_manager", + "data_viewer", + }, + } + + def __init__(self, repository: UserRepository, role_repository: RoleRepository): + super().__init__(UserModel, repository) + self.role_repository = role_repository + self.forbidden_exception = HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You are not authorized to access this resource", + ) + + async def find_by_username(self, username: str) -> UserModel | None: + user = await self.repository.find_by_username(username) + if not user: + raise NotFoundException("User not found") + return user + + async def find_by_email(self, email: str) -> UserModel | None: + user = await self.repository.find_by_email(email) + if not user: + raise NotFoundException("User not found") + return user + + @override + async def find_all( + self, + user: UserSchema, + filters: Union[str, list[str]] = None, + sort: Union[str, list[str]] = None, + search: str = "", + group_by: str = None, + limit: int = 100, + offset: int = 0, + relationships: List[str] = None, + searchable_columns: List[str] = None, + ) -> Tuple[List[UserModel], int]: + if user.role.name not in self.access_control_level["administrator"]: + raise self.forbidden_exception + + role_instances = await self.role_repository.get_list_by_names(self.access_control_level[user.role.name]) + temp_filters = [] + for role in role_instances: + temp_filters.append(f"role_id={role.id}") + + filters.append(temp_filters) + return await super().find_all( + filters, + sort, + search, + group_by, + limit, + offset, + relationships, + searchable_columns, + ) + + async def find_by_id(self, id: UUID, user: UserSchema = None) -> UserSchema | None: + + user_instance = await self.repository.find_by_id(id) + if not user_instance: + raise NotFoundException("User not found") + + if user is not None: + if user.role.name not in self.access_control_level["administrator"]: + raise self.forbidden_exception + if user_instance.role.name not in self.access_control_level[user.role.name]: + raise self.forbidden_exception + + return user_instance + + async def create(self, user_data: Dict, user: UserSchema) -> UserModel: + role_instance = await self.role_repository.find_by_id(user.role.id) + + if not role_instance: + raise NotFoundException("Role not found") + role_name = role_instance.name + + if role_name not in ["administrator", "data_validator"]: + raise self.forbidden_exception + + if await self.repository.find_by_username(user_data["username"]): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already exists", + ) + if await self.repository.find_by_email(user_data["email"]): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists") + + user_data["password"] = get_password_hash(user_data["password"]) + return await self.repository.create(user_data) + + async def update(self, id: UUID, user_data: Dict, user: UserSchema) -> UserModel: + if user.role.name not in self.access_control_level["administrator"]: + raise self.forbidden_exception + + user_instance = await self.find_by_id(id) + if not user_instance: + raise NotFoundException("User not found") + + if user_instance.role.name not in self.access_control_level[user.role.name]: + raise self.forbidden_exception + + if "username" in user_data and await self.repository.find_by_username(user_data["username"]): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already exists", + ) + if "email" in user_data and await self.repository.find_by_email(user_data["email"]): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists") + + if "password" in user_data: + user_data["password"] = get_password_hash(user_data["password"]) + + return await self.repository.update(id, user_data) + + async def bulk_update_activation(self, user_ids: List[UUID], is_active: bool, user: UserSchema) -> None: + if user.role.name not in self.access_control_level["administrator"]: + raise self.forbidden_exception + + existing_user_ids = await self.repository.find_all_ids(user_ids) + for user_id in user_ids: + if user_id not in existing_user_ids: + raise NotFoundException(f"User with id {user_id} not found") + + await self.repository.bulk_update_activation(user_ids, is_active) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/encryption.py b/app/utils/encryption.py new file mode 100644 index 0000000..4be6ddc --- /dev/null +++ b/app/utils/encryption.py @@ -0,0 +1,66 @@ +import base64 +import json +import os +from typing import Dict + +from cryptography.fernet import Fernet +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + +from app.core.config import settings + + +class CredentialEncryption: + """ + Kelas untuk mengenkripsi dan mendekripsi data kredensial. + + Menggunakan Fernet (implementasi AES-128-CBC) dengan salt dan PBKDF2 + untuk meningkatkan keamanan. + """ + + def __init__(self): + self.master_key = settings.SECRET_KEY + + def _derive_key(self, salt): + master_key_bytes = self.master_key.encode() + kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000, backend=default_backend()) + key = base64.urlsafe_b64encode(kdf.derive(master_key_bytes)) + return key + + def encrypt(self, data): + iv = os.urandom(16) + iv_b64 = base64.b64encode(iv).decode("utf-8") + + key = self._derive_key(iv) + + cipher = Fernet(key) + + data_json = json.dumps(data) + + encrypted_data = cipher.encrypt(data_json.encode("utf-8")) + encrypted_b64 = base64.b64encode(encrypted_data).decode("utf-8") + + return encrypted_b64, iv_b64 + + def decrypt(self, encrypted_data, iv) -> Dict: + try: + encrypted_bytes = base64.b64decode(encrypted_data) + iv_bytes = base64.b64decode(iv) + + key = self._derive_key(iv_bytes) + + cipher = Fernet(key) + + decrypted_data = cipher.decrypt(encrypted_bytes) + decrypted_str = decrypted_data.decode("utf-8") + + data = json.loads(decrypted_str) + + return data + except Exception as e: + raise e + + +# Inisialisasi singleton instance +credential_encryption = CredentialEncryption() diff --git a/app/utils/helpers.py b/app/utils/helpers.py new file mode 100644 index 0000000..8e1af48 --- /dev/null +++ b/app/utils/helpers.py @@ -0,0 +1,35 @@ +from typing import Any + +import orjson + + +def orm_to_dict(orm_instance): + """Convert SQLAlchemy ORM instance to dictionary.""" + if orm_instance is None: + return None + + result = {} + for key in orm_instance.__mapper__.c.keys(): + result[key] = getattr(orm_instance, key) + + # Handle relationships if needed + for relationship in orm_instance.__mapper__.relationships: + rel_name = relationship.key + rel_value = getattr(orm_instance, rel_name) + if rel_value is not None: + # Check if it's a collection + if hasattr(rel_value, "__iter__") and not isinstance(rel_value, str): + result[rel_name] = [orm_to_dict(item) for item in rel_value] + else: + result[rel_name] = orm_to_dict(rel_value) + + return result + + +def orjson_dumps(__obj: Any, *, default=None) -> str: + """Custom JSON serializer using orjson.""" + return orjson.dumps( + __obj, + default=default, + option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_SERIALIZE_UUID | orjson.OPT_UTC_Z, + ).decode("utf-8") diff --git a/app/utils/system.py b/app/utils/system.py new file mode 100644 index 0000000..a12cd6c --- /dev/null +++ b/app/utils/system.py @@ -0,0 +1,37 @@ +import multiprocessing +import os +import resource + +import psutil + + +def get_optimal_workers(): + """ + Menghitung jumlah worker optimal berdasarkan CPU, RAM, dan karakteristik aplikasi + """ + cpu_count = multiprocessing.cpu_count() + workers_by_cpu = (2 * cpu_count) + 1 + + available_ram = psutil.virtual_memory().available / (1024 * 1024) + reserved_ram = 512 + ram_per_worker = 100 + + max_workers_by_ram = int((available_ram - reserved_ram) / ram_per_worker) + + optimal_workers = min(workers_by_cpu, max_workers_by_ram) + optimal_workers = max(2, optimal_workers) + optimal_workers = min(12, optimal_workers) + + return optimal_workers + + +async def optimize_system(): + """Lakukan optimasi sistem untuk performa maksimal.""" + + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + target = 65536 + resource.setrlimit(resource.RLIMIT_NOFILE, (min(target, hard), hard)) + + os.environ["TCP_NODELAY"] = "1" + + os.environ["TCP_QUICKACK"] = "1" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..48f6b91 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,75 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: satu-peta-postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: satupeta + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "5432:5432" + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + minio: + image: minio/minio:latest + container_name: satu-peta-minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 10s + timeout: 5s + retries: 5 + + migration: + build: + context: . + dockerfile: Dockerfile + container_name: satu-peta-migration + env_file: + - environment.env + command: sh -c "alembic current | grep -q '(head)' && echo 'Database already migrated' || alembic upgrade head" + depends_on: + postgres: + condition: service_healthy + restart: "no" + + api: + build: + context: . + dockerfile: Dockerfile + container_name: satu-peta-api + restart: unless-stopped + ports: + - "5000:5000" + env_file: + - environment.env + command: uvicorn app.main:app --host 0.0.0.0 --port 5000 --reload + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_healthy + migration: + condition: service_completed_successfully + +volumes: + minio_data: diff --git a/environment.env b/environment.env new file mode 100644 index 0000000..bb5eb29 --- /dev/null +++ b/environment.env @@ -0,0 +1,12 @@ +DEBUG=1 +HOST=0.0.0.0 +PORT=5000 +DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/satupeta +SECRET_KEY=tgAjg3fj4y4DtPCuIOlUH8cWFSxx9VZqvbXFsOiMnsmo2FF6NU +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +MINIO_ENDPOINT_URL=minio:9000 +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin123 diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..2500aa1 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..ce51ecb --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,79 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import create_async_engine + +from app.core.config import settings +from app.core.database import Base + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Set the SQLAlchemy URL from settings +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def do_run_migrations(connection): + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + Calls to context.execute() here emit the given string to the + script output. + """ + config.get_main_option("sqlalchemy.url") + context.configure( + url=settings.DATABASE_URL, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): + """Run migrations in 'online' mode. + In this scenario we need to create an Engine + and associate a connection with the context. + """ + connectable = create_async_engine(settings.DATABASE_URL, poolclass=pool.NullPool) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..480b130 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/migrations/scripts.py b/migrations/scripts.py new file mode 100644 index 0000000..99b4475 --- /dev/null +++ b/migrations/scripts.py @@ -0,0 +1,29 @@ +import asyncio +import os +import shutil + +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine + + +async def drop_alembic_version_table(): + engine = create_async_engine(os.getenv("DATABASE_URL")) + async with engine.begin() as conn: + await conn.execute(text("DROP TABLE IF EXISTS alembic_version CASCADE")) + + +def migrate(): + os.system("alembic revision --autogenerate -m 'auto generated migration'") + os.system("alembic upgrade head") + + +if __name__ == "__main__": + version_folder = "migrations/versions" + if not os.path.exists(version_folder): + os.mkdir(version_folder) + try: + asyncio.run(drop_alembic_version_table()) + migrate() + finally: + if os.path.exists(version_folder): + shutil.rmtree(version_folder) diff --git a/migrations/versions/20241203_1200_initial_schema.py b/migrations/versions/20241203_1200_initial_schema.py new file mode 100644 index 0000000..f2b4986 --- /dev/null +++ b/migrations/versions/20241203_1200_initial_schema.py @@ -0,0 +1,355 @@ +"""initial schema + +Revision ID: initial_schema +Revises: +Create Date: 2024-12-03 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'initial_schema' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create categories table + op.create_table('categories', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('thumbnail', sa.String(), nullable=True), + sa.Column('count_mapset', sa.Integer(), server_default='0', nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('order', sa.Integer(), server_default='0', nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_categories_id', 'categories', ['id']) + + # Create classifications table + op.create_table('classifications', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=20), nullable=False), + sa.Column('is_open', sa.Boolean(), nullable=True), + sa.Column('is_limited', sa.Boolean(), nullable=True), + sa.Column('is_secret', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_classifications_id', 'classifications', ['id']) + + # Create map_projection_systems table + op.create_table('map_projection_systems', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_map_projection_systems_id', 'map_projection_systems', ['id']) + + # Create news table + op.create_table('news', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('thumbnail', sa.String(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_news_id', 'news', ['id']) + + # Create organizations table + op.create_table('organizations', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('thumbnail', sa.String(length=255), nullable=True), + sa.Column('address', sa.String(length=255), nullable=True), + sa.Column('phone_number', sa.String(length=15), nullable=True), + sa.Column('email', sa.String(length=100), nullable=True), + sa.Column('website', sa.String(length=255), nullable=True), + sa.Column('is_active', sa.Boolean(), server_default='true', nullable=True), + sa.Column('is_deleted', sa.Boolean(), server_default='false', nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('modified_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_organizations_id', 'organizations', ['id']) + + # Create regionals table + op.create_table('regionals', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('code', sa.String(length=10), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('thumbnail', sa.String(length=255), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_regionals_id', 'regionals', ['id']) + + # Create roles table + op.create_table('roles', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=20), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_roles_id', 'roles', ['id']) + + # Create users table (depends on roles and organizations) + op.create_table('users', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('profile_picture', sa.String(), nullable=True), + sa.Column('username', sa.String(), nullable=False), + sa.Column('password', sa.String(), nullable=False), + sa.Column('position', sa.String(), nullable=True), + sa.Column('role_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('employee_id', sa.String(), nullable=True), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('modified_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_index('ix_users_id', 'users', ['id']) + + # Create credentials table (depends on users) + op.create_table('credentials', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('encrypted_data', sa.Text(), nullable=False), + sa.Column('encryption_iv', sa.String(length=255), nullable=False), + sa.Column('credential_type', sa.String(length=50), nullable=False), + sa.Column('credential_metadata', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('updated_by', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_used_by', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['last_used_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_credentials_id', 'credentials', ['id']) + + # Create files table (depends on users) + op.create_table('files', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('object_name', sa.String(length=512), nullable=False), + sa.Column('content_type', sa.String(length=100), nullable=False), + sa.Column('size', sa.Integer(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('url', sa.String(length=1024), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('modified_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('object_name') + ) + op.create_index('ix_files_filename', 'files', ['filename']) + op.create_index('ix_files_id', 'files', ['id']) + + # Create refresh_tokens table (depends on users) + op.create_table('refresh_tokens', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('token', sa.String(length=255), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('revoked', sa.Boolean(), server_default='false', nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_refresh_tokens_id', 'refresh_tokens', ['id']) + op.create_index('ix_refresh_tokens_token', 'refresh_tokens', ['token']) + op.create_index('ix_refresh_tokens_user_id', 'refresh_tokens', ['user_id']) + + # Create map_sources table (depends on credentials) + op.create_table('map_sources', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('credential_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('url', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['credential_id'], ['credentials.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_map_sources_id', 'map_sources', ['id']) + + # Create mapsets table (depends on multiple tables) + op.create_table('mapsets', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('scale', sa.String(length=29), nullable=False), + sa.Column('layer_url', sa.Text(), nullable=True), + sa.Column('metadata_url', sa.Text(), nullable=True), + sa.Column('category_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('classification_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('regional_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('projection_system_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('producer_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('status_validation', sa.String(length=20), nullable=True), + sa.Column('data_status', sa.String(length=20), nullable=False), + sa.Column('data_update_period', sa.String(length=20), nullable=False), + sa.Column('data_version', sa.String(length=20), nullable=False), + sa.Column('coverage_level', sa.String(length=20), nullable=True), + sa.Column('coverage_area', sa.String(length=20), nullable=True), + sa.Column('is_popular', sa.Boolean(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('updated_by', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('layer_type', sa.String(length=20), nullable=True), + sa.Column('view_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('download_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('order', sa.Integer(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ), + sa.ForeignKeyConstraint(['classification_id'], ['classifications.id'], ), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['producer_id'], ['organizations.id'], ), + sa.ForeignKeyConstraint(['projection_system_id'], ['map_projection_systems.id'], ), + sa.ForeignKeyConstraint(['regional_id'], ['regionals.id'], ), + sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_mapsets_id', 'mapsets', ['id']) + + # Create mapset_access table (depends on mapsets, users, and organizations) + op.create_table('mapset_access', + sa.Column('id', sa.String(), nullable=False), + sa.Column('mapset_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('granted_by', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('can_read', sa.Boolean(), nullable=True), + sa.Column('can_write', sa.Boolean(), nullable=True), + sa.Column('can_delete', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['granted_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['mapset_id'], ['mapsets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create mapset_histories table (depends on mapsets and users) + op.create_table('mapset_histories', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('mapset_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('validation_type', sa.String(length=50), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['mapset_id'], ['mapsets.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_mapset_histories_id', 'mapset_histories', ['id']) + op.create_index('ix_mapset_histories_mapset_id', 'mapset_histories', ['mapset_id']) + + # Create source_usages table (depends on map_sources and mapsets) + op.create_table('source_usages', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('source_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('mapset_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['mapset_id'], ['mapsets.id'], ), + sa.ForeignKeyConstraint(['source_id'], ['map_sources.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_source_usages_id', 'source_usages', ['id']) + + # Create feedback table (independent) + op.create_table('feedback', + sa.Column('id', sa.Integer(), nullable=False, autoincrement=True), + sa.Column('datetime', sa.DateTime(), nullable=True), + sa.Column('score', sa.Integer(), nullable=True), + sa.Column('tujuan_tercapai', sa.Boolean(), nullable=True), + sa.Column('tujuan_ditemukan', sa.Boolean(), nullable=True), + sa.Column('tujuan', sa.String(length=255), nullable=True), + sa.Column('sektor', sa.String(length=255), nullable=True), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('saran', sa.Text(), nullable=True), + sa.Column('source_url', sa.String(length=255), nullable=True), + sa.Column('source_access', sa.String(length=255), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('gender', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade() -> None: + # Drop tables in reverse order to respect foreign key constraints + op.drop_table('feedback') + op.drop_index('ix_source_usages_id', table_name='source_usages') + op.drop_table('source_usages') + op.drop_index('ix_mapset_histories_mapset_id', table_name='mapset_histories') + op.drop_index('ix_mapset_histories_id', table_name='mapset_histories') + op.drop_table('mapset_histories') + op.drop_table('mapset_access') + op.drop_index('ix_mapsets_id', table_name='mapsets') + op.drop_table('mapsets') + op.drop_index('ix_map_sources_id', table_name='map_sources') + op.drop_table('map_sources') + op.drop_index('ix_refresh_tokens_user_id', table_name='refresh_tokens') + op.drop_index('ix_refresh_tokens_token', table_name='refresh_tokens') + op.drop_index('ix_refresh_tokens_id', table_name='refresh_tokens') + op.drop_table('refresh_tokens') + op.drop_index('ix_files_id', table_name='files') + op.drop_index('ix_files_filename', table_name='files') + op.drop_table('files') + op.drop_index('ix_credentials_id', table_name='credentials') + op.drop_table('credentials') + op.drop_index('ix_users_id', table_name='users') + op.drop_table('users') + op.drop_index('ix_roles_id', table_name='roles') + op.drop_table('roles') + op.drop_index('ix_regionals_id', table_name='regionals') + op.drop_table('regionals') + op.drop_index('ix_organizations_id', table_name='organizations') + op.drop_table('organizations') + op.drop_index('ix_news_id', table_name='news') + op.drop_table('news') + op.drop_index('ix_map_projection_systems_id', table_name='map_projection_systems') + op.drop_table('map_projection_systems') + op.drop_index('ix_classifications_id', table_name='classifications') + op.drop_table('classifications') + op.drop_index('ix_categories_id', table_name='categories') + op.drop_table('categories') diff --git a/migrations/versions/20241204_0000_seed_initial_data.py b/migrations/versions/20241204_0000_seed_initial_data.py new file mode 100644 index 0000000..0e24f87 --- /dev/null +++ b/migrations/versions/20241204_0000_seed_initial_data.py @@ -0,0 +1,114 @@ +"""seed initial data + +Revision ID: 20241204_0000 +Revises: initial_schema +Create Date: 2024-12-04 00:00:00.000000 + +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa +from datetime import datetime, timezone +from app.core.security import get_password_hash + +# revision identifiers, used by Alembic. +revision: str = '20241204_0000' +down_revision: Union[str, None] = 'initial_schema' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Seed roles + op.execute(""" + INSERT INTO roles (id, name, description, is_active) + VALUES + ('0196a031-a540-78ca-a0d2-ccfb6fb6823c', 'data_viewer', 'Contoh deskripsi', true), + ('01968027-048b-7166-9b71-412e178f65ee', 'administrator', 'Contoh deskripsi', true), + ('0196802b-b5e8-740b-9f0c-b7e423aff947', 'data_validator', 'Contoh deskripsi', true), + ('01969f62-aae2-7b59-9e23-da29223f70e1', 'data_manager', 'Contoh deskripsi', true) + ON CONFLICT (id) DO NOTHING; + """) + + # Seed regional + op.execute(""" + INSERT INTO regionals (id, code, name, description, thumbnail, is_active, created_at, updated_at) + VALUES ( + '01968b53-a910-7a67-bd10-975b8923b92e', + '35.00', + 'Jawa Timur', + 'Provinsi Jawa Timur', + NULL, + true, + '2025-04-29 13:04:51.693000+00', + '2025-04-29 13:04:51.693000+00' + ) + ON CONFLICT (id) DO NOTHING; + """) + + # Seed organization + op.execute(""" + INSERT INTO organizations (id, name, description, thumbnail, address, phone_number, email, website, is_active, is_deleted, created_at, modified_at) + VALUES ( + '01968b54-0000-7a67-bd10-975b8923b92e', + 'Kominfo', + 'Kementerian Komunikasi dan Informatika', + NULL, + NULL, + NULL, + 'info@kominfo.go.id', + 'https://www.kominfo.go.id', + true, + false, + NOW(), + NOW() + ) + ON CONFLICT (id) DO NOTHING; + """) + + # Seed user admin + # Password: admin123 (hashed with bcrypt using get_password_hash) + hashed_password = get_password_hash("admin123") + op.execute(f""" + INSERT INTO users ( + id, + name, + email, + profile_picture, + username, + password, + position, + role_id, + employee_id, + organization_id, + is_active, + is_deleted, + created_at, + modified_at + ) + VALUES ( + '01968b55-0000-7a67-bd10-975b8923b92e', + 'Administrator Kominfo', + 'admin@kominfo.go.id', + NULL, + 'admin', + '{hashed_password}', + 'System Administrator', + '01968027-048b-7166-9b71-412e178f65ee', + NULL, + '01968b54-0000-7a67-bd10-975b8923b92e', + true, + false, + NOW(), + NOW() + ) + ON CONFLICT (username) DO NOTHING; + """) + + +def downgrade() -> None: + # Delete in reverse order due to foreign key constraints + op.execute("DELETE FROM users WHERE username = 'admin';") + op.execute("DELETE FROM organizations WHERE name = 'Kominfo';") + op.execute("DELETE FROM regionals WHERE code = '35.00';") + op.execute("DELETE FROM roles WHERE name IN ('data_viewer', 'administrator', 'data_validator', 'data_manager');") diff --git a/migrations/versions/__init__.py b/migrations/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a4788e8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3300 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "aiofile" +version = "3.9.0" +description = "Asynchronous file operations." +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa"}, + {file = "aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b"}, +] + +[package.dependencies] +caio = ">=0.9.0,<0.10.0" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.18" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"}, + {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6"}, + {file = "aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868"}, + {file = "aiohttp-3.11.18-cp310-cp310-win32.whl", hash = "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f"}, + {file = "aiohttp-3.11.18-cp310-cp310-win_amd64.whl", hash = "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9"}, + {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9"}, + {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b"}, + {file = "aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd"}, + {file = "aiohttp-3.11.18-cp311-cp311-win32.whl", hash = "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d"}, + {file = "aiohttp-3.11.18-cp311-cp311-win_amd64.whl", hash = "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6"}, + {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2"}, + {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508"}, + {file = "aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea"}, + {file = "aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8"}, + {file = "aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8"}, + {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811"}, + {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804"}, + {file = "aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7"}, + {file = "aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78"}, + {file = "aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01"}, + {file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:469ac32375d9a716da49817cd26f1916ec787fc82b151c1c832f58420e6d3533"}, + {file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cec21dd68924179258ae14af9f5418c1ebdbba60b98c667815891293902e5e0"}, + {file = "aiohttp-3.11.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b426495fb9140e75719b3ae70a5e8dd3a79def0ae3c6c27e012fc59f16544a4a"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2f41203e2808616292db5d7170cccf0c9f9c982d02544443c7eb0296e8b0c7"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc0ae0a5e9939e423e065a3e5b00b24b8379f1db46046d7ab71753dfc7dd0e1"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7cdd3f7d1df43200e1c80f1aed86bb36033bf65e3c7cf46a2b97a253ef8798"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5199be2a2f01ffdfa8c3a6f5981205242986b9e63eb8ae03fd18f736e4840721"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ccec9e72660b10f8e283e91aa0295975c7bd85c204011d9f5eb69310555cf30"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1596ebf17e42e293cbacc7a24c3e0dc0f8f755b40aff0402cb74c1ff6baec1d3"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:eab7b040a8a873020113ba814b7db7fa935235e4cbaf8f3da17671baa1024863"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5d61df4a05476ff891cff0030329fee4088d40e4dc9b013fac01bc3c745542c2"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:46533e6792e1410f9801d09fd40cbbff3f3518d1b501d6c3c5b218f427f6ff08"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c1b90407ced992331dd6d4f1355819ea1c274cc1ee4d5b7046c6761f9ec11829"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a2fd04ae4971b914e54fe459dd7edbbd3f2ba875d69e057d5e3c8e8cac094935"}, + {file = "aiohttp-3.11.18-cp39-cp39-win32.whl", hash = "sha256:b2f317d1678002eee6fe85670039fb34a757972284614638f82b903a03feacdc"}, + {file = "aiohttp-3.11.18-cp39-cp39-win_amd64.whl", hash = "sha256:5e7007b8d1d09bce37b54111f593d173691c530b80f27c6493b928dabed9e6ef"}, + {file = "aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiohttp-retry" +version = "2.9.1" +description = "Simple retry client for aiohttp" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54"}, + {file = "aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1"}, +] + +[package.dependencies] +aiohttp = "*" + +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "alembic" +version = "1.15.2" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53"}, + {file = "alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.4.0" +typing-extensions = ">=4.12" + +[package.extras] +tz = ["tzdata"] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.9.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "argcomplete" +version = "3.5.3" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, + {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "asyncpg" +version = "0.30.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"}, + {file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"}, + {file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"}, + {file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"}, + {file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[package.extras] +docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + +[[package]] +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotli-asgi" +version = "1.4.0" +description = "A compression AGSI middleware using brotli" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "brotli-asgi-1.4.0.tar.gz", hash = "sha256:e2d6eca4b97af19716af05bbe0d4d79cffb0b0947100c188da9ed089f5b7bc2f"}, + {file = "brotli_asgi-1.4.0-py3-none-any.whl", hash = "sha256:bc18bcb20803dc9d11abdd14ac4242f2f09ebb9d2a67f4d3a7fe4cb8bedcd226"}, +] + +[package.dependencies] +brotli = ">=1.0.9" +starlette = ">=0.25.0" + +[package.extras] +test-brotli = ["mypy (>=0.770)", "requests (>=2.23.0)"] +test-brotlipy = ["brotlipy (>=0.7.0)", "mypy (>=0.770)", "requests (>=2.23.0)"] + +[[package]] +name = "caio" +version = "0.9.24" +description = "Asynchronous file IO for Linux MacOS or Windows." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "caio-0.9.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80322126a97ba572412b17b2f086ff95195de2c4261deb19db6bfcdc9ef7540"}, + {file = "caio-0.9.24-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:37bc172349686139e8dc97fff7662c67b1837e18a67b99e8ef25585f2893d013"}, + {file = "caio-0.9.24-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ad7f0902bf952237e120606252c14ab3cb05995c9f79f39154b5248744864832"}, + {file = "caio-0.9.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:925b9e3748ce1a79386dfb921c0aee450e43225534551abd1398b1c08f9ba29f"}, + {file = "caio-0.9.24-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:3b4dc0a8fb9a58ab40f967ad5a8a858cc0bfb2348a580b4142595849457f9c9a"}, + {file = "caio-0.9.24-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fa74d111b3b165bfad2e333367976bdf118bcf505a1cb44d3bcddea2849e3297"}, + {file = "caio-0.9.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae3566228383175265a7583107f21a7cb044a752ea29ba84fce7c1a49a05903"}, + {file = "caio-0.9.24-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:a306b0dda91cb4ca3170f066c114597f8ea41b3da578574a9d2b54f86963de68"}, + {file = "caio-0.9.24-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8ee158e56128d865fb7d57a9c9c22fca4e8aa8d8664859c977a36fff3ccb3609"}, + {file = "caio-0.9.24-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d47ef8d76aca74c17cb07339a441c5530fc4b8dd9222dfb1e1abd7f9f9b814f"}, + {file = "caio-0.9.24-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:d15fc746c4bf0077d75df05939d1e97c07ccaa8e580681a77021d6929f65d9f4"}, + {file = "caio-0.9.24-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9368eae0a9badd5f31264896c51b47431d96c0d46f1979018fb1d20c49f56156"}, + {file = "caio-0.9.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f0e5a645ef4e7bb7a81e10ae2a7aef14988cb2cb4354588c6bf6f6f3f6de72a"}, + {file = "caio-0.9.24-cp39-cp39-manylinux_2_34_aarch64.whl", hash = "sha256:08304fa80af7771c78a5bcc923449c7ec8134d589b50d48c66320f85552c7ae2"}, + {file = "caio-0.9.24-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:5339ced0764e10242a50ccb21db7f0d9c359881db0f72fa2c5e45ed828ffacf7"}, + {file = "caio-0.9.24.tar.gz", hash = "sha256:5bcdecaea02a9aa8e3acf0364eff8ad9903d57d70cdb274a42270126290a77f1"}, +] + +[package.extras] +develop = ["aiomisc-pytest", "coveralls", "pylama[toml]", "pytest", "pytest-cov", "setuptools"] + +[[package]] +name = "certifi" +version = "2025.4.26" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "colour" +version = "0.1.5" +description = "converts and manipulates various color representation (HSL, RVB, web, X11, ...)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"}, + {file = "colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"}, +] + +[package.extras] +test = ["nose"] + +[[package]] +name = "commitizen" +version = "4.6.0" +description = "Python commitizen client tool" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +files = [ + {file = "commitizen-4.6.0-py3-none-any.whl", hash = "sha256:d8861707b553c03c8b1993b7abd9e036384fdd1c57f95f6f38d5f215c53041a9"}, + {file = "commitizen-4.6.0.tar.gz", hash = "sha256:cc1c9f8937e59a7c54321443aa49dd246e07b829e305c7cbff1d7f7e32e449fe"}, +] + +[package.dependencies] +argcomplete = ">=1.12.1,<3.6" +charset-normalizer = ">=2.1.0,<4" +colorama = ">=0.4.1,<1.0" +decli = ">=0.6.0,<1.0" +jinja2 = ">=2.10.3" +packaging = ">=19" +pyyaml = ">=3.08" +questionary = ">=2.0,<3.0" +termcolor = ">=1.1,<3" +tomlkit = ">=0.5.3,<1.0.0" + +[[package]] +name = "coverage" +version = "7.8.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cryptography" +version = "44.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] +files = [ + {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, + {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, + {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, + {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, + {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, + {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, + {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "decli" +version = "0.6.2" +description = "Minimal, easy-to-use, declarative cli tool" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed"}, + {file = "decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "ecdsa" +version = "0.19.1" +description = "ECDSA cryptographic signature library (pure python)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" +groups = ["main"] +files = [ + {file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"}, + {file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"}, +] + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "email-validator" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "fastapi" +version = "0.115.12" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastapi-async-sqlalchemy" +version = "0.6.1" +description = "SQLAlchemy middleware for FastAPI" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "fastapi-async-sqlalchemy-0.6.1.tar.gz", hash = "sha256:c4e0c9832e5e7ef9d647e7eb134e6d326945dca28323e503a21f3d4ab2dee160"}, + {file = "fastapi_async_sqlalchemy-0.6.1-py3-none-any.whl", hash = "sha256:0f4edfbc7b0f5fc2e0017cd903a953f4e0b01870f09e86cd0bc79087f3606bc4"}, +] + +[package.dependencies] +SQLAlchemy = ">=1.4.19" +starlette = ">=0.13.6" + +[[package]] +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + +[[package]] +name = "frozenlist" +version = "1.6.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, + {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, + {file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"}, + {file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"}, + {file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"}, + {file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"}, + {file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"}, + {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"}, + {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"}, + {file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"}, + {file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"}, + {file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"}, + {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"}, + {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"}, + {file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"}, + {file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"}, + {file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"}, + {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"}, + {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"}, + {file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"}, + {file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"}, + {file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"}, + {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0"}, + {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70"}, + {file = "frozenlist-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c"}, + {file = "frozenlist-1.6.0-cp39-cp39-win32.whl", hash = "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530"}, + {file = "frozenlist-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572"}, + {file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"}, + {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, +] + +[[package]] +name = "greenlet" +version = "3.2.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "greenlet-3.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:777c1281aa7c786738683e302db0f55eb4b0077c20f1dc53db8852ffaea0a6b0"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3059c6f286b53ea4711745146ffe5a5c5ff801f62f6c56949446e0f6461f8157"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1a40a17e2c7348f5eee5d8e1b4fa6a937f0587eba89411885a36a8e1fc29bd2"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5193135b3a8d0017cb438de0d49e92bf2f6c1c770331d24aa7500866f4db4017"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639a94d001fe874675b553f28a9d44faed90f9864dc57ba0afef3f8d76a18b04"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fe303381e7e909e42fb23e191fc69659910909fdcd056b92f6473f80ef18543"}, + {file = "greenlet-3.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:72c9b668454e816b5ece25daac1a42c94d1c116d5401399a11b77ce8d883110c"}, + {file = "greenlet-3.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6079ae990bbf944cf66bea64a09dcb56085815630955109ffa98984810d71565"}, + {file = "greenlet-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e63cd2035f49376a23611fbb1643f78f8246e9d4dfd607534ec81b175ce582c2"}, + {file = "greenlet-3.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aa30066fd6862e1153eaae9b51b449a6356dcdb505169647f69e6ce315b9468b"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b0f3a0a67786facf3b907a25db80efe74310f9d63cc30869e49c79ee3fcef7e"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64a4d0052de53ab3ad83ba86de5ada6aeea8f099b4e6c9ccce70fb29bc02c6a2"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852ef432919830022f71a040ff7ba3f25ceb9fe8f3ab784befd747856ee58530"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4818116e75a0dd52cdcf40ca4b419e8ce5cb6669630cb4f13a6c384307c9543f"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9afa05fe6557bce1642d8131f87ae9462e2a8e8c46f7ed7929360616088a3975"}, + {file = "greenlet-3.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5c12f0d17a88664757e81a6e3fc7c2452568cf460a2f8fb44f90536b2614000b"}, + {file = "greenlet-3.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dbb4e1aa2000852937dd8f4357fb73e3911da426df8ca9b8df5db231922da474"}, + {file = "greenlet-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:cb5ee928ce5fedf9a4b0ccdc547f7887136c4af6109d8f2fe8e00f90c0db47f5"}, + {file = "greenlet-3.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0ba2811509a30e5f943be048895a983a8daf0b9aa0ac0ead526dfb5d987d80ea"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4245246e72352b150a1588d43ddc8ab5e306bef924c26571aafafa5d1aaae4e8"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7abc0545d8e880779f0c7ce665a1afc3f72f0ca0d5815e2b006cafc4c1cc5840"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dcc6d604a6575c6225ac0da39df9335cc0c6ac50725063fa90f104f3dbdb2c9"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2273586879affca2d1f414709bb1f61f0770adcabf9eda8ef48fd90b36f15d12"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff38c869ed30fff07f1452d9a204ece1ec6d3c0870e0ba6e478ce7c1515acf22"}, + {file = "greenlet-3.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e934591a7a4084fa10ee5ef50eb9d2ac8c4075d5c9cf91128116b5dca49d43b1"}, + {file = "greenlet-3.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:063bcf7f8ee28eb91e7f7a8148c65a43b73fbdc0064ab693e024b5a940070145"}, + {file = "greenlet-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7132e024ebeeeabbe661cf8878aac5d2e643975c4feae833142592ec2f03263d"}, + {file = "greenlet-3.2.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e1967882f0c42eaf42282a87579685c8673c51153b845fde1ee81be720ae27ac"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e77ae69032a95640a5fe8c857ec7bee569a0997e809570f4c92048691ce4b437"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3227c6ec1149d4520bc99edac3b9bc8358d0034825f3ca7572165cb502d8f29a"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ddda0197c5b46eedb5628d33dad034c455ae77708c7bf192686e760e26d6a0c"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de62b542e5dcf0b6116c310dec17b82bb06ef2ceb696156ff7bf74a7a498d982"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07a0c01010df42f1f058b3973decc69c4d82e036a951c3deaf89ab114054c07"}, + {file = "greenlet-3.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2530bfb0abcd451ea81068e6d0a1aac6dabf3f4c23c8bd8e2a8f579c2dd60d95"}, + {file = "greenlet-3.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c472adfca310f849903295c351d297559462067f618944ce2650a1878b84123"}, + {file = "greenlet-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:24a496479bc8bd01c39aa6516a43c717b4cee7196573c47b1f8e1011f7c12495"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175d583f7d5ee57845591fc30d852b75b144eb44b05f38b67966ed6df05c8526"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ecc9d33ca9428e4536ea53e79d781792cee114d2fa2695b173092bdbd8cd6d5"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f56382ac4df3860ebed8ed838f268f03ddf4e459b954415534130062b16bc32"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc45a7189c91c0f89aaf9d69da428ce8301b0fd66c914a499199cfb0c28420fc"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51a2f49da08cff79ee42eb22f1658a2aed60c72792f0a0a95f5f0ca6d101b1fb"}, + {file = "greenlet-3.2.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:0c68bbc639359493420282d2f34fa114e992a8724481d700da0b10d10a7611b8"}, + {file = "greenlet-3.2.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:e775176b5c203a1fa4be19f91da00fd3bff536868b77b237da3f4daa5971ae5d"}, + {file = "greenlet-3.2.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d6668caf15f181c1b82fb6406f3911696975cc4c37d782e19cb7ba499e556189"}, + {file = "greenlet-3.2.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:17964c246d4f6e1327edd95e2008988a8995ae3a7732be2f9fc1efed1f1cdf8c"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b4ec7f65f0e4a1500ac475c9343f6cc022b2363ebfb6e94f416085e40dea15"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b38d53cf268da963869aa25a6e4cc84c1c69afc1ae3391738b2603d110749d01"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a7490f74e8aabc5f29256765a99577ffde979920a2db1f3676d265a3adba41"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4339b202ac20a89ccd5bde0663b4d00dc62dd25cb3fb14f7f3034dec1b0d9ece"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a750f1046994b9e038b45ae237d68153c29a3a783075211fb1414a180c8324b"}, + {file = "greenlet-3.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:374ffebaa5fbd10919cd599e5cf8ee18bae70c11f9d61e73db79826c8c93d6f9"}, + {file = "greenlet-3.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b89e5d44f55372efc6072f59ced5ed1efb7b44213dab5ad7e0caba0232c6545"}, + {file = "greenlet-3.2.1-cp39-cp39-win32.whl", hash = "sha256:b7503d6b8bbdac6bbacf5a8c094f18eab7553481a1830975799042f26c9e101b"}, + {file = "greenlet-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:e98328b8b8f160925d6b1c5b1879d8e64f6bd8cf11472b7127d579da575b77d9"}, + {file = "greenlet-3.2.1.tar.gz", hash = "sha256:9f4dd4b4946b14bb3bf038f81e1d2e535b7d94f1b2a59fdba1293cd9c1a0a4d7"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "h2" +version = "4.2.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "hyperframe" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, +] + +[[package]] +name = "identify" +version = "2.6.10" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25"}, + {file = "identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "mako" +version = "1.3.10" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "miniopy-async" +version = "1.22.1" +description = "Asynchronous MinIO Client SDK for Python" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "miniopy_async-1.22.1-py3-none-any.whl", hash = "sha256:b665ef0f64c9b1968eac9b15cf400001a2d4b10543102ab91c8b184ac15fdf5b"}, + {file = "miniopy_async-1.22.1.tar.gz", hash = "sha256:7c2c7f6fe10f9f1cdb585fcafb6662de16ccfa0669afabec502fcbba70baf029"}, +] + +[package.dependencies] +aiofile = ">=3.9.0,<4.0.0" +aiohttp = ">=3.11.14,<4.0.0" +aiohttp-retry = ">=2.9.1,<3.0.0" +argon2-cffi = ">=23.1.0,<24.0.0" +certifi = ">=2025.1.31,<2026.0.0" +pycryptodome = ">=3.22.0,<4.0.0" + +[[package]] +name = "multidict" +version = "6.4.3" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"}, + {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188"}, + {file = "multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5"}, + {file = "multidict-6.4.3-cp310-cp310-win32.whl", hash = "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e"}, + {file = "multidict-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7"}, + {file = "multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378"}, + {file = "multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589"}, + {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676"}, + {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1"}, + {file = "multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a"}, + {file = "multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124"}, + {file = "multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db"}, + {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474"}, + {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd"}, + {file = "multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8"}, + {file = "multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3"}, + {file = "multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5"}, + {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6"}, + {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c"}, + {file = "multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4"}, + {file = "multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5"}, + {file = "multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208"}, + {file = "multidict-6.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21"}, + {file = "multidict-6.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b"}, + {file = "multidict-6.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df"}, + {file = "multidict-6.4.3-cp39-cp39-win32.whl", hash = "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f"}, + {file = "multidict-6.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897"}, + {file = "multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9"}, + {file = "multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "2.2.5" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "numpy-2.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f4a922da1729f4c40932b2af4fe84909c7a6e167e6e99f71838ce3a29f3fe26"}, + {file = "numpy-2.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6f91524d31b34f4a5fee24f5bc16dcd1491b668798b6d85585d836c1e633a6a"}, + {file = "numpy-2.2.5-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:19f4718c9012e3baea91a7dba661dcab2451cda2550678dc30d53acb91a7290f"}, + {file = "numpy-2.2.5-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:eb7fd5b184e5d277afa9ec0ad5e4eb562ecff541e7f60e69ee69c8d59e9aeaba"}, + {file = "numpy-2.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6413d48a9be53e183eb06495d8e3b006ef8f87c324af68241bbe7a39e8ff54c3"}, + {file = "numpy-2.2.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7451f92eddf8503c9b8aa4fe6aa7e87fd51a29c2cfc5f7dbd72efde6c65acf57"}, + {file = "numpy-2.2.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0bcb1d057b7571334139129b7f941588f69ce7c4ed15a9d6162b2ea54ded700c"}, + {file = "numpy-2.2.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36ab5b23915887543441efd0417e6a3baa08634308894316f446027611b53bf1"}, + {file = "numpy-2.2.5-cp310-cp310-win32.whl", hash = "sha256:422cc684f17bc963da5f59a31530b3936f57c95a29743056ef7a7903a5dbdf88"}, + {file = "numpy-2.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:e4f0b035d9d0ed519c813ee23e0a733db81ec37d2e9503afbb6e54ccfdee0fa7"}, + {file = "numpy-2.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c42365005c7a6c42436a54d28c43fe0e01ca11eb2ac3cefe796c25a5f98e5e9b"}, + {file = "numpy-2.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:498815b96f67dc347e03b719ef49c772589fb74b8ee9ea2c37feae915ad6ebda"}, + {file = "numpy-2.2.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6411f744f7f20081b1b4e7112e0f4c9c5b08f94b9f086e6f0adf3645f85d3a4d"}, + {file = "numpy-2.2.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9de6832228f617c9ef45d948ec1cd8949c482238d68b2477e6f642c33a7b0a54"}, + {file = "numpy-2.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:369e0d4647c17c9363244f3468f2227d557a74b6781cb62ce57cf3ef5cc7c610"}, + {file = "numpy-2.2.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:262d23f383170f99cd9191a7c85b9a50970fe9069b2f8ab5d786eca8a675d60b"}, + {file = "numpy-2.2.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa70fdbdc3b169d69e8c59e65c07a1c9351ceb438e627f0fdcd471015cd956be"}, + {file = "numpy-2.2.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37e32e985f03c06206582a7323ef926b4e78bdaa6915095ef08070471865b906"}, + {file = "numpy-2.2.5-cp311-cp311-win32.whl", hash = "sha256:f5045039100ed58fa817a6227a356240ea1b9a1bc141018864c306c1a16d4175"}, + {file = "numpy-2.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:b13f04968b46ad705f7c8a80122a42ae8f620536ea38cf4bdd374302926424dd"}, + {file = "numpy-2.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ee461a4eaab4f165b68780a6a1af95fb23a29932be7569b9fab666c407969051"}, + {file = "numpy-2.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec31367fd6a255dc8de4772bd1658c3e926d8e860a0b6e922b615e532d320ddc"}, + {file = "numpy-2.2.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:47834cde750d3c9f4e52c6ca28a7361859fcaf52695c7dc3cc1a720b8922683e"}, + {file = "numpy-2.2.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2c1a1c6ccce4022383583a6ded7bbcda22fc635eb4eb1e0a053336425ed36dfa"}, + {file = "numpy-2.2.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d75f338f5f79ee23548b03d801d28a505198297534f62416391857ea0479571"}, + {file = "numpy-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a801fef99668f309b88640e28d261991bfad9617c27beda4a3aec4f217ea073"}, + {file = "numpy-2.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:abe38cd8381245a7f49967a6010e77dbf3680bd3627c0fe4362dd693b404c7f8"}, + {file = "numpy-2.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a0ac90e46fdb5649ab6369d1ab6104bfe5854ab19b645bf5cda0127a13034ae"}, + {file = "numpy-2.2.5-cp312-cp312-win32.whl", hash = "sha256:0cd48122a6b7eab8f06404805b1bd5856200e3ed6f8a1b9a194f9d9054631beb"}, + {file = "numpy-2.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:ced69262a8278547e63409b2653b372bf4baff0870c57efa76c5703fd6543282"}, + {file = "numpy-2.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4"}, + {file = "numpy-2.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f"}, + {file = "numpy-2.2.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9"}, + {file = "numpy-2.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191"}, + {file = "numpy-2.2.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372"}, + {file = "numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d"}, + {file = "numpy-2.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7"}, + {file = "numpy-2.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73"}, + {file = "numpy-2.2.5-cp313-cp313-win32.whl", hash = "sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b"}, + {file = "numpy-2.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471"}, + {file = "numpy-2.2.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6"}, + {file = "numpy-2.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba"}, + {file = "numpy-2.2.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133"}, + {file = "numpy-2.2.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376"}, + {file = "numpy-2.2.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19"}, + {file = "numpy-2.2.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0"}, + {file = "numpy-2.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a"}, + {file = "numpy-2.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066"}, + {file = "numpy-2.2.5-cp313-cp313t-win32.whl", hash = "sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e"}, + {file = "numpy-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8"}, + {file = "numpy-2.2.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4ea7e1cff6784e58fe281ce7e7f05036b3e1c89c6f922a6bfbc0a7e8768adbe"}, + {file = "numpy-2.2.5-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d7543263084a85fbc09c704b515395398d31d6395518446237eac219eab9e55e"}, + {file = "numpy-2.2.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0255732338c4fdd00996c0421884ea8a3651eea555c3a56b84892b66f696eb70"}, + {file = "numpy-2.2.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2e3bdadaba0e040d1e7ab39db73e0afe2c74ae277f5614dad53eadbecbbb169"}, + {file = "numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291"}, +] + +[[package]] +name = "orjson" +version = "3.10.16" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"}, + {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"}, + {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370"}, + {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b"}, + {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06"}, + {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c"}, + {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15"}, + {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da"}, + {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e"}, + {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4"}, + {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551"}, + {file = "orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd"}, + {file = "orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055"}, + {file = "orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739"}, + {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225"}, + {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741"}, + {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53"}, + {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14"}, + {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c"}, + {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca"}, + {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50"}, + {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1"}, + {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d"}, + {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164"}, + {file = "orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619"}, + {file = "orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60"}, + {file = "orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca"}, + {file = "orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184"}, + {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a"}, + {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef"}, + {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e"}, + {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa"}, + {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4"}, + {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b"}, + {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42"}, + {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87"}, + {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88"}, + {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e"}, + {file = "orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c"}, + {file = "orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6"}, + {file = "orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd"}, + {file = "orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8"}, + {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137"}, + {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b"}, + {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90"}, + {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e"}, + {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb"}, + {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0"}, + {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652"}, + {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56"}, + {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430"}, + {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5"}, + {file = "orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6"}, + {file = "orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7"}, + {file = "orjson-3.10.16-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c35b5c1fb5a5d6d2fea825dec5d3d16bea3c06ac744708a8e1ff41d4ba10cdf1"}, + {file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9aac7ecc86218b4b3048c768f227a9452287001d7548500150bb75ee21bf55d"}, + {file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e19f5102fff36f923b6dfdb3236ec710b649da975ed57c29833cb910c5a73ab"}, + {file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17210490408eb62755a334a6f20ed17c39f27b4f45d89a38cd144cd458eba80b"}, + {file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbbe04451db85916e52a9f720bd89bf41f803cf63b038595674691680cbebd1b"}, + {file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a966eba501a3a1f309f5a6af32ed9eb8f316fa19d9947bac3e6350dc63a6f0a"}, + {file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01e0d22f06c81e6c435723343e1eefc710e0510a35d897856766d475f2a15687"}, + {file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7c1e602d028ee285dbd300fb9820b342b937df64d5a3336e1618b354e95a2569"}, + {file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d230e5020666a6725629df81e210dc11c3eae7d52fe909a7157b3875238484f3"}, + {file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0f8baac07d4555f57d44746a7d80fbe6b2c4fe2ed68136b4abb51cfec512a5e9"}, + {file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:524e48420b90fc66953e91b660b3d05faaf921277d6707e328fde1c218b31250"}, + {file = "orjson-3.10.16-cp39-cp39-win32.whl", hash = "sha256:a9f614e31423d7292dbca966a53b2d775c64528c7d91424ab2747d8ab8ce5c72"}, + {file = "orjson-3.10.16-cp39-cp39-win_amd64.whl", hash = "sha256:c338dc2296d1ed0d5c5c27dfb22d00b330555cb706c2e0be1e1c3940a0895905"}, + {file = "orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] +totp = ["cryptography"] + +[[package]] +name = "platformdirs" +version = "4.3.7" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.2.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "propcache" +version = "0.3.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, + {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, + {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, + {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, + {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, + {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, + {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, + {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, + {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, + {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, + {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, + {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, + {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, + {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, + {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, +] + +[[package]] +name = "psutil" +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, +] + +[package.extras] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pycryptodome" +version = "3.22.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:96e73527c9185a3d9b4c6d1cfb4494f6ced418573150be170f6580cb975a7f5a"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9e1bb165ea1dc83a11e5dbbe00ef2c378d148f3a2d3834fb5ba4e0f6fd0afe4b"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:d4d1174677855c266eed5c4b4e25daa4225ad0c9ffe7584bb1816767892545d0"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:9dbb749cef71c28271484cbef684f9b5b19962153487735411e1020ca3f59cb1"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f1ae7beb64d4fc4903a6a6cca80f1f448e7a8a95b77d106f8a29f2eb44d17547"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a26bcfee1293b7257c83b0bd13235a4ee58165352be4f8c45db851ba46996dc6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:98fd9da809d5675f3a65dcd9ed384b9dc67edab6a4cda150c5870a8122ec961d"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:37ddcd18284e6b36b0a71ea495a4c4dca35bb09ccc9bfd5b91bfaf2321f131c1"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56c6f9342fcb6c74e205fbd2fee568ec4cdbdaa6165c8fde55dbc4ba5f584464"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87a88dc543b62b5c669895caf6c5a958ac7abc8863919e94b7a6cafd2f64064f"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a683bc9fa585c0dfec7fa4801c96a48d30b30b096e3297f9374f40c2fedafc"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f4f6f47a7f411f2c157e77bbbda289e0c9f9e1e9944caa73c1c2e33f3f92d6e"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6cf9553b29624961cab0785a3177a333e09e37ba62ad22314ebdbb01ca79840"}, + {file = "pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723"}, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"}, + {file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.33.1" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"}, + {file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde"}, + {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65"}, + {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc"}, + {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091"}, + {file = "pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383"}, + {file = "pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504"}, + {file = "pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24"}, + {file = "pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77"}, + {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961"}, + {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1"}, + {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c"}, + {file = "pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896"}, + {file = "pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83"}, + {file = "pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89"}, + {file = "pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8"}, + {file = "pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b"}, + {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39"}, + {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a"}, + {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db"}, + {file = "pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda"}, + {file = "pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4"}, + {file = "pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea"}, + {file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"}, + {file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"}, + {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"}, + {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"}, + {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"}, + {file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"}, + {file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"}, + {file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"}, + {file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"}, + {file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"}, + {file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"}, + {file = "pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb"}, + {file = "pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6"}, + {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d"}, + {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96"}, + {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599"}, + {file = "pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5"}, + {file = "pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3"}, + {file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, + {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, + {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-jose" +version = "3.4.0" +description = "JOSE implementation in Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python-jose-3.4.0.tar.gz", hash = "sha256:9a9a40f418ced8ecaf7e3b28d69887ceaa76adad3bcaa6dae0d9e596fec1d680"}, + {file = "python_jose-3.4.0-py2.py3-none-any.whl", hash = "sha256:9c9f616819652d109bd889ecd1e15e9a162b9b94d682534c9c2146092945b78f"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = ">=0.4.1,<0.5.0" +rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "questionary" +version = "2.1.0" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, + {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + +[[package]] +name = "rsa" +version = "4.2" +description = "Pure-Python RSA implementation" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "rsa-4.2.tar.gz", hash = "sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "shapely" +version = "2.1.0" +description = "Manipulation and analysis of geometric objects" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "shapely-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3e5c5e3864d4dc431dd85a8e5137ebd39c8ac287b009d3fa80a07017b29c940"}, + {file = "shapely-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6eea89b16f5f3a064659126455d23fa3066bc3d6cd385c35214f06bf5871aa6"}, + {file = "shapely-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:183174ad0b21a81ee661f05e7c47aa92ebfae01814cd3cbe54adea7a4213f5f4"}, + {file = "shapely-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f239c1484af66bc14b81a76f2a8e0fada29d59010423253ff857d0ccefdaa93f"}, + {file = "shapely-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6220a466d1475141dad0cd8065d2549a5c2ed3fa4e2e02fb8ea65d494cfd5b07"}, + {file = "shapely-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4822d3ed3efb06145c34d29d5b56792f72b7d713300f603bfd5d825892c6f79f"}, + {file = "shapely-2.1.0-cp310-cp310-win32.whl", hash = "sha256:ea51ddf3d3c60866dca746081b56c75f34ff1b01acbd4d44269071a673c735b9"}, + {file = "shapely-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6f5e02e2cded9f4ec5709900a296c7f2cce5f8e9e9d80ba7d89ae2f4ed89d7b"}, + {file = "shapely-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8323031ef7c1bdda7a92d5ddbc7b6b62702e73ba37e9a8ccc8da99ec2c0b87c"}, + {file = "shapely-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4da7c6cd748d86ec6aace99ad17129d30954ccf5e73e9911cdb5f0fa9658b4f8"}, + {file = "shapely-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f0cdf85ff80831137067e7a237085a3ee72c225dba1b30beef87f7d396cf02b"}, + {file = "shapely-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f2be5d79aac39886f23000727cf02001aef3af8810176c29ee12cdc3ef3a50"}, + {file = "shapely-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21a4515009f56d7a159cf5c2554264e82f56405b4721f9a422cb397237c5dca8"}, + {file = "shapely-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cebc323cec2cb6b2eaa310fdfc621f6dbbfaf6bde336d13838fcea76c885a9"}, + {file = "shapely-2.1.0-cp311-cp311-win32.whl", hash = "sha256:cad51b7a5c8f82f5640472944a74f0f239123dde9a63042b3c5ea311739b7d20"}, + {file = "shapely-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4005309dde8658e287ad9c435c81877f6a95a9419b932fa7a1f34b120f270ae"}, + {file = "shapely-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53e7ee8bd8609cf12ee6dce01ea5affe676976cf7049315751d53d8db6d2b4b2"}, + {file = "shapely-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cab20b665d26dbec0b380e15749bea720885a481fa7b1eedc88195d4a98cfa4"}, + {file = "shapely-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a38b39a09340273c3c92b3b9a374272a12cc7e468aeeea22c1c46217a03e5c"}, + {file = "shapely-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edaec656bdd9b71278b98e6f77c464b1c3b2daa9eace78012ff0f0b4b5b15b04"}, + {file = "shapely-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c8a732ddd9b25e7a54aa748e7df8fd704e23e5d5d35b7d376d80bffbfc376d04"}, + {file = "shapely-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c93693ad8adfdc9138a5a2d42da02da94f728dd2e82d2f0f442f10e25027f5f"}, + {file = "shapely-2.1.0-cp312-cp312-win32.whl", hash = "sha256:d8ac6604eefe807e71a908524de23a37920133a1729fe3a4dfe0ed82c044cbf4"}, + {file = "shapely-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f47e631aa4f9ec5576eac546eb3f38802e2f82aeb0552f9612cb9a14ece1db"}, + {file = "shapely-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b64423295b563f43a043eb786e7a03200ebe68698e36d2b4b1c39f31dfb50dfb"}, + {file = "shapely-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1b5578f45adc25b235b22d1ccb9a0348c8dc36f31983e57ea129a88f96f7b870"}, + {file = "shapely-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a7e83d383b27f02b684e50ab7f34e511c92e33b6ca164a6a9065705dd64bcb"}, + {file = "shapely-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:942031eb4d8f7b3b22f43ba42c09c7aa3d843aa10d5cc1619fe816e923b66e55"}, + {file = "shapely-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2843c456a2e5627ee6271800f07277c0d2652fb287bf66464571a057dbc00b3"}, + {file = "shapely-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8c4b17469b7f39a5e6a7cfea79f38ae08a275427f41fe8b48c372e1449147908"}, + {file = "shapely-2.1.0-cp313-cp313-win32.whl", hash = "sha256:30e967abd08fce49513d4187c01b19f139084019f33bec0673e8dbeb557c45e4"}, + {file = "shapely-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:1dc8d4364483a14aba4c844b7bd16a6fa3728887e2c33dfa1afa34a3cf4d08a5"}, + {file = "shapely-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:673e073fea099d1c82f666fb7ab0a00a77eff2999130a69357ce11941260d855"}, + {file = "shapely-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d1513f915a56de67659fe2047c1ad5ff0f8cbff3519d1e74fced69c9cb0e7da"}, + {file = "shapely-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d6a7043178890b9e028d80496ff4c79dc7629bff4d78a2f25323b661756bab8"}, + {file = "shapely-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb638378dc3d76f7e85b67d7e2bb1366811912430ac9247ac00c127c2b444cdc"}, + {file = "shapely-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:737124e87d91d616acf9a911f74ac55e05db02a43a6a7245b3d663817b876055"}, + {file = "shapely-2.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e6c229e7bb87aae5df82fa00b6718987a43ec168cc5affe095cca59d233f314"}, + {file = "shapely-2.1.0-cp313-cp313t-win32.whl", hash = "sha256:a9580bda119b1f42f955aa8e52382d5c73f7957e0203bc0c0c60084846f3db94"}, + {file = "shapely-2.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e8ff4e5cfd799ba5b6f37b5d5527dbd85b4a47c65b6d459a03d0962d2a9d4d10"}, + {file = "shapely-2.1.0.tar.gz", hash = "sha256:2cbe90e86fa8fc3ca8af6ffb00a77b246b918c7cf28677b7c21489b678f6b02e"}, +] + +[package.dependencies] +numpy = ">=1.21" + +[package.extras] +docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] +test = ["pytest", "pytest-cov", "scipy-doctest"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.40" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bece9527f5a98466d67fb5d34dc560c4da964240d8b09024bb21c1246545e04e"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:8bb131ffd2165fae48162c7bbd0d97c84ab961deea9b8bab16366543deeab625"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9408fd453d5f8990405cc9def9af46bfbe3183e6110401b407c2d073c3388f47"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win32.whl", hash = "sha256:00a494ea6f42a44c326477b5bee4e0fc75f6a80c01570a32b57e89cf0fbef85a"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win_amd64.whl", hash = "sha256:c7b927155112ac858357ccf9d255dd8c044fd9ad2dc6ce4c4149527c901fa4c3"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win32.whl", hash = "sha256:cd2f75598ae70bcfca9117d9e51a3b06fe29edd972fdd7fd57cc97b4dbf3b08a"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win_amd64.whl", hash = "sha256:2cbafc8d39ff1abdfdda96435f38fab141892dc759a2165947d1a8fffa7ef596"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50f5885bbed261fc97e2e66c5156244f9704083a674b8d17f24c72217d29baf5"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf0e99cdb600eabcd1d65cdba0d3c91418fee21c4aa1d28db47d095b1064a7d8"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe147fcd85aaed53ce90645c91ed5fca0cc88a797314c70dfd9d35925bd5d106"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf7cee56bd552385c1ee39af360772fbfc2f43be005c78d1140204ad6148438"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4aeb939bcac234b88e2d25d5381655e8353fe06b4e50b1c55ecffe56951d18c2"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c268b5100cfeaa222c40f55e169d484efa1384b44bf9ca415eae6d556f02cb08"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win32.whl", hash = "sha256:46628ebcec4f23a1584fb52f2abe12ddb00f3bb3b7b337618b80fc1b51177aff"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win_amd64.whl", hash = "sha256:7e0505719939e52a7b0c65d20e84a6044eb3712bb6f239c6b1db77ba8e173a37"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c884de19528e0fcd9dc34ee94c810581dd6e74aef75437ff17e696c2bfefae3e"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1abb387710283fc5983d8a1209d9696a4eae9db8d7ac94b402981fe2fe2e39ad"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfa124eda500ba4b0d3afc3e91ea27ed4754e727c7f025f293a22f512bcd4c9"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6b28d303b9d57c17a5164eb1fd2d5119bb6ff4413d5894e74873280483eeb5"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b5a5bbe29c10c5bfd63893747a1bf6f8049df607638c786252cb9243b86b6706"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f0fda83e113bb0fb27dc003685f32a5dcb99c9c4f41f4fa0838ac35265c23b5c"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win32.whl", hash = "sha256:957f8d85d5e834397ef78a6109550aeb0d27a53b5032f7a57f2451e1adc37e98"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win_amd64.whl", hash = "sha256:1ffdf9c91428e59744f8e6f98190516f8e1d05eec90e936eb08b257332c5e870"}, + {file = "sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a"}, + {file = "sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "termcolor" +version = "2.5.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.4.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uuid6" +version = "2024.7.10" +description = "New time-based UUID formats which are suited for use as a database key" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7"}, + {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, + {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.21.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "virtualenv" +version = "20.30.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "watchfiles" +version = "1.0.5" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, + {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"}, + {file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"}, + {file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"}, + {file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"}, + {file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"}, + {file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"}, + {file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"}, + {file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"}, + {file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"}, + {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[[package]] +name = "yarl" +version = "1.20.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, + {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, + {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, + {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, + {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, + {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, + {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, + {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, + {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, + {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, + {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, + {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, + {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, + {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, + {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13,<4.0" +content-hash = "94531c84d75ff2d704a041af8f0b5b23ca38025c9113afeb5a03d02992b83dad" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..efffa87 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[tool.poetry] +name = "portal-satu-peta-backend" +version = "0.1.0" +description = "Backend untuk Portal Data Satu Peta" +authors = ["Trikintech "] +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.13,<4.0" +fastapi = ">=0.115.12,<0.116.0" +uvicorn = {extras = ["standard"], version = ">=0.34.1,<0.35.0"} +pydantic = {extras = ["email"], version = ">=2.11.3,<3.0.0"} +pydantic-settings = ">=2.8.1,<3.0.0" +python-jose = {extras = ["cryptography"], version = ">=3.4.0,<4.0.0"} +passlib = {extras = ["bcrypt"], version = ">=1.7.4,<2.0.0"} +alembic = ">=1.15.2,<2.0.0" +orjson = ">=3.10.16,<4.0.0" +uuid6 = ">=2024.7.10,<2025.0.0" +fastapi-async-sqlalchemy = ">=0.6.1,<0.7.0" +asyncpg = ">=0.30.0,<0.31.0" +greenlet = ">=3.2.1,<4.0.0" +brotli-asgi = ">=1.4.0,<2.0.0" +miniopy-async = ">=1.22.1,<2.0.0" +urllib3 = ">=2.4.0,<3.0.0" +python-multipart = ">=0.0.20,<0.0.21" +itsdangerous = ">=2.2.0,<3.0.0" +pytz = "^2025.2" +shapely = "^2.1.0" +colour = "^0.1.5" +httpx = {extras = ["http2"], version = "^0.28.1"} +psutil = "^7.0.0" + + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.5" +pytest-asyncio = "^0.26.0" +httpx = "^0.28.1" +pytest-cov = "^6.1.1" +pre-commit = "^4.2.0" +commitizen = {version = "^4.6.0", python = ">=3.13,<4.0"} + + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +python_functions = "test_*" +python_classes = "Test*" + +[tool.mypy] +python_version = "3.10" +plugins = ["pydantic.mypy"] +disallow_untyped_defs = true +disallow_incomplete_defs = true + +[tool.black] +line-length = 88 +target-version = ["py310"] + +[tool.isort] +profile = "black" +line_length = 88 diff --git a/run.py b/run.py new file mode 100644 index 0000000..94ce941 --- /dev/null +++ b/run.py @@ -0,0 +1,24 @@ +import uvicorn + +from app.core.config import settings + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=settings.HOST, + port=settings.PORT, + workers=settings.WORKERS, + loop=settings.LOOP, + http=settings.HTTP, + log_level=settings.LOG_LEVEL, + reload=settings.DEBUG, + limit_concurrency=settings.LIMIT_CONCURRENCY, + backlog=settings.BACKLOG, + limit_max_requests=settings.LIMIT_MAX_REQUESTS, + timeout_keep_alive=settings.TIMEOUT_KEEP_ALIVE, + access_log=settings.ACCESS_LOG, + h11_max_incomplete_event_size=settings.H11_MAX_INCOMPLETE_EVENT_SIZE, + server_header=settings.SERVER_HEADER, + date_header=settings.DATE_HEADER, + forwarded_allow_ips=settings.FORWARDED_ALLOW_IPS, + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_api/__init__.py b/tests/test_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_services/__init__.py b/tests/test_services/__init__.py new file mode 100644 index 0000000..e69de29