diff --git a/.docs/images/format.png b/.docs/images/format.png index 409de27..7dd3784 100644 Binary files a/.docs/images/format.png and b/.docs/images/format.png differ diff --git a/.docs/images/pre-commit.png b/.docs/images/pre-commit.png new file mode 100644 index 0000000..32a203f Binary files /dev/null and b/.docs/images/pre-commit.png differ diff --git a/.flake8 b/.flake8 deleted file mode 100755 index e30440e..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 130 -per-file-ignores = - __init__.py: F401 -exclude = - .git,__pycache__,src/alembic/versions diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..9deaa82 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,113 @@ +name: CI Pipeline Template + +'on': + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + lint-and-format: + name: Run Linters & Formatters + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.13 + + - name: Install dependencies + run: | + pip install poetry + poetry install --no-root --with dev + + - name: Install pre-commit + run: pip install pre-commit + - name: Run linters and formatters + run: pre-commit run --all-files + + tests: + name: Run Tests + runs-on: ubuntu-latest + needs: lint-and-format + timeout-minutes: 15 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: dev + POSTGRES_PASSWORD: dev + POSTGRES_DB: dev + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U dev -d dev" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + PROJECT_NAME: python-template + DATABASE_URL: postgresql://dev:dev@localhost:5432/dev + ASYNC_DATABASE_URL: postgresql+asyncpg://dev:dev@localhost:5432/dev + DATABASE_POOL_PRE_PING: True + DATABASE_POOL_SIZE: 5 + DATABASE_POOL_RECYCLE: 3600 + DATABASE_MAX_OVERFLOW: 10 + LOG_LEVEL: DEBUG + SERVER_URL: example.com + ACCESS_TOKEN_EXPIRE_MINUTES: 15 + JWT_SIGNING_KEY: your-signing-key + CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672 + CELERY_RESULT_BACKEND: redis://redis:6379/0 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.13 + + - name: Install dependencies + run: | + pip install poetry + poetry install --no-root --with dev + + - name: Run tests with coverage + run: | + poetry run coverage run -m pytest + poetry run coverage report -m --fail-under=80 + + docker-build: + name: Build Docker Image + runs-on: ubuntu-latest + needs: tests + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-docker-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-docker- + + - name: Build image with cache + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: python-template:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache,mode=max diff --git a/.gitignore b/.gitignore index 2f9a178..b073d48 100755 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ logs/* .ptpython-history .mypy_cache .pytest_cache +.coverage +*.coverage .devcontainer/commandhistory !.devcontainer/commandhistory/.gitkeep diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100755 index b64472e..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[settings] -profile=black -skip=alembic diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0dcb817 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +# See https://pre-commit.com/hooks.html for more hooks +default_stages: [pre-commit, pre-push] +files: ^src/|^alembic/versions/ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.5.5 + hooks: + # Run the linter. + - id: ruff + args: [ --select=I, --fix ] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.8.0' + hooks: + - id: mypy + exclude: ^src/alembic/|^scripts/|^\\.devcontainer/|^\\.github/ + additional_dependencies: [ + pydantic==2.10.6, + SQLAlchemy==2.0.39, + types-mock==5.2.0.20250306 + ] diff --git a/CODEOWNERS b/CODEOWNERS index ca85fcb..f973a99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,4 +2,4 @@ # the repo. Unless a later match takes precedence, # @pgrill, @m-revetria, @dcousin24 and @nicoache1 will be requested for # review when someone opens a pull request. -* @pgrill @m-revetria @dcousin24 @nicoache1 \ No newline at end of file +* @pgrill @m-revetria @dcousin24 @nicoache1 diff --git a/README.md b/README.md index 3dcc846..c2c8a6a 100755 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Alternatively, you must have: - [Poetry](https://python-poetry.org/docs/#installation) (don't forget to install the dependencies from the lock file) - [PostgreSQL](https://www.postgresql.org/) database, setting the corresponding environment variables for the database connection. +For making code changes, installing `pre-commit` is necessary (see section [Code tools: pre-commit](#pre-commit)) + ### Customization The project's name (`python-template`) can be edited following next steps: @@ -50,11 +52,25 @@ We use Alembic as database migration tool. You can run migration commands direct ## Code tools Linters, formatters, etc. -- **Pycln**: Formatter for finding and removing unused import statements. -- **isort**: Tool to sort imports alphabetically and automatically separate into sections by type. -- **flake8**: Linting tool +- **ruff**: Linter and formatter - **mypy**: Static type checker -- **black**: PEP 8 compliant opinionated formatter + +### pre-commit +`pre-commit` is part of the `dev` group in the `pyproject.toml` and is installed by default. + +Setup the `pre-commit` hooks, specified in `.pre-commit-config.yaml`: + + pre-commit install + +Ensure everything was set up correctly by running the hooks: + + pre-commit run --all-files + +![Screenshot](.docs/images/pre-commit.png) + +#### Adding hooks + +You can add new `pre-commit` hooks by editing `.pre-commit-config.yaml`. Whenever new hooks are added, you must run `pre-commit install` to ensure new hooks are run on commit. There is a shortcut under the `/scripts` directory that runs all this tools for you (`./exec.sh format`) or just run `format` inside the dev container. diff --git a/mypy.ini b/mypy.ini deleted file mode 100755 index 893bc6c..0000000 --- a/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -plugins = pydantic.mypy, sqlalchemy.ext.mypy.plugin -disallow_untyped_defs = True -exclude = src/alembic/ diff --git a/poetry.lock b/poetry.lock index d7b5561..d834b47 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alembic" @@ -65,7 +65,7 @@ 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)", "uvloop (>=0.21)"] +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]] @@ -141,8 +141,8 @@ files = [ [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] -gssauth = ["gssapi", "sspilib"] -test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] +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 = "bcrypt" @@ -221,51 +221,6 @@ files = [ {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, ] -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "celery" version = "5.4.0" @@ -293,32 +248,32 @@ vine = ">=5.1.0,<6.0" arangodb = ["pyArango (>=2.0.2)"] auth = ["cryptography (==42.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] -brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] -couchbase = ["couchbase (>=3.0.0)"] +couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] -eventlet = ["eventlet (>=0.32.0)"] +eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] gcs = ["google-cloud-storage (>=2.10.0)"] gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=2.0.0)"] -memcache = ["pylibmc (==1.6.3)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] mongodb = ["pymongo[srv] (>=4.0.2)"] msgpack = ["msgpack (==1.0.8)"] pymemcache = ["python-memcached (>=1.61)"] -pyro = ["pyro4 (==4.82)"] +pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] pytest = ["pytest-celery[all] (>=1.0.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.5)"] +solar = ["ephem (==4.1.5) ; platform_python_implementation != \"PyPy\""] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] -tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] @@ -350,6 +305,18 @@ files = [ {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] +[[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 = "click" version = "8.1.8" @@ -430,6 +397,94 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +[[package]] +name = "coverage" +version = "7.7.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:553ba93f8e3c70e1b0031e4dfea36aba4e2b51fe5770db35e99af8dc5c5a9dfe"}, + {file = "coverage-7.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44683f2556a56c9a6e673b583763096b8efbd2df022b02995609cf8e64fc8ae0"}, + {file = "coverage-7.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02fad4f8faa4153db76f9246bc95c1d99f054f4e0a884175bff9155cf4f856cb"}, + {file = "coverage-7.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c181ceba2e6808ede1e964f7bdc77bd8c7eb62f202c63a48cc541e5ffffccb6"}, + {file = "coverage-7.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b5b207a8b08c6a934b214e364cab2fa82663d4af18981a6c0a9e95f8df7602"}, + {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:25fe40967717bad0ce628a0223f08a10d54c9d739e88c9cbb0f77b5959367542"}, + {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:881cae0f9cbd928c9c001487bb3dcbfd0b0af3ef53ae92180878591053be0cb3"}, + {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90e9141e9221dd6fbc16a2727a5703c19443a8d9bf7d634c792fa0287cee1ab"}, + {file = "coverage-7.7.1-cp310-cp310-win32.whl", hash = "sha256:ae13ed5bf5542d7d4a0a42ff5160e07e84adc44eda65ddaa635c484ff8e55917"}, + {file = "coverage-7.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:171e9977c6a5d2b2be9efc7df1126fd525ce7cad0eb9904fe692da007ba90d81"}, + {file = "coverage-7.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1165490be0069e34e4f99d08e9c5209c463de11b471709dfae31e2a98cbd49fd"}, + {file = "coverage-7.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44af11c00fd3b19b8809487630f8a0039130d32363239dfd15238e6d37e41a48"}, + {file = "coverage-7.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbba59022e7c20124d2f520842b75904c7b9f16c854233fa46575c69949fb5b9"}, + {file = "coverage-7.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af94fb80e4f159f4d93fb411800448ad87b6039b0500849a403b73a0d36bb5ae"}, + {file = "coverage-7.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae79f8e3501133aa0e220bbc29573910d096795882a70e6f6e6637b09522133"}, + {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e33426a5e1dc7743dd54dfd11d3a6c02c5d127abfaa2edd80a6e352b58347d1a"}, + {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b559adc22486937786731dac69e57296cb9aede7e2687dfc0d2696dbd3b1eb6b"}, + {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b838a91e84e1773c3436f6cc6996e000ed3ca5721799e7789be18830fad009a2"}, + {file = "coverage-7.7.1-cp311-cp311-win32.whl", hash = "sha256:2c492401bdb3a85824669d6a03f57b3dfadef0941b8541f035f83bbfc39d4282"}, + {file = "coverage-7.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e6f867379fd033a0eeabb1be0cffa2bd660582b8b0c9478895c509d875a9d9e"}, + {file = "coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d"}, + {file = "coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f"}, + {file = "coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1"}, + {file = "coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0"}, + {file = "coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2"}, + {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af"}, + {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59"}, + {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2"}, + {file = "coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8"}, + {file = "coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04"}, + {file = "coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585"}, + {file = "coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c"}, + {file = "coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019"}, + {file = "coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf"}, + {file = "coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b"}, + {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f"}, + {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777"}, + {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a"}, + {file = "coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a"}, + {file = "coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1"}, + {file = "coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511"}, + {file = "coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24"}, + {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950"}, + {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d"}, + {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e"}, + {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862"}, + {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271"}, + {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de"}, + {file = "coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c"}, + {file = "coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c"}, + {file = "coverage-7.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a3bf6b92e6621fc4dcdaab353e173ccb0ca9e4bfbcf7e49a0134c86c9cd303"}, + {file = "coverage-7.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6874929d624d3a670f676efafbbc747f519a6121b581dd41d012109e70a5ebd"}, + {file = "coverage-7.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5ff236c87a7b7aa1441a216caf44baee14cbfbd2256d306f926d16b026578"}, + {file = "coverage-7.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452735fafe8ff5918236d5fe1feac322b359e57692269c75151f9b4ee4b7e1bc"}, + {file = "coverage-7.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5f99a93cecf799738e211f9746dc83749b5693538fbfac279a61682ba309387"}, + {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11dd6f52c2a7ce8bf0a5f3b6e4a8eb60e157ffedc3c4b4314a41c1dfbd26ce58"}, + {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b52edb940d087e2a96e73c1523284a2e94a4e66fa2ea1e2e64dddc67173bad94"}, + {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2e73e2ac468536197e6b3ab79bc4a5c9da0f078cd78cfcc7fe27cf5d1195ef0"}, + {file = "coverage-7.7.1-cp39-cp39-win32.whl", hash = "sha256:18f544356bceef17cc55fcf859e5664f06946c1b68efcea6acdc50f8f6a6e776"}, + {file = "coverage-7.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d66ff48ab3bb6f762a153e29c0fc1eb5a62a260217bc64470d7ba602f5886d20"}, + {file = "coverage-7.7.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:5b7b02e50d54be6114cc4f6a3222fec83164f7c42772ba03b520138859b5fde1"}, + {file = "coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31"}, + {file = "coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[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" @@ -541,21 +596,21 @@ sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.22)"] tortoise = ["tortoise-orm (>=0.22.0)"] [[package]] -name = "flake8" -version = "7.1.2" -description = "the modular source code checker: pep8 pyflakes and co" +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." optional = false -python-versions = ">=3.8.1" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, - {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" +[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 = "flower" @@ -717,7 +772,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +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.*)"] @@ -738,6 +793,21 @@ files = [ [package.extras] tests = ["freezegun", "pytest", "pytest-cov"] +[[package]] +name = "identify" +version = "2.6.9" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, + {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.10" @@ -765,22 +835,6 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - [[package]] name = "itsdangerous" version = "2.2.0" @@ -853,7 +907,7 @@ azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] -librabbitmq = ["librabbitmq (>=2.0.0)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] @@ -861,63 +915,10 @@ qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] -[[package]] -name = "libcst" -version = "1.7.0" -description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "libcst-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:340054c57abcd42953248af18ed278be651a03b1c2a1616f7e1f1ef90b6018ce"}, - {file = "libcst-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdae6e632d222d8db7cb98d7cecb45597c21b8e3841d0c98d4fca79c49dad04b"}, - {file = "libcst-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8f59f3472fe8c0f6e2fad457825ea2ccad8c4c713cca55a91ff2cbfa9bc03"}, - {file = "libcst-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1560598f5c56681adbd32f4b08e9cffcd45a021921d1d784370a7d4d9a2fac11"}, - {file = "libcst-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cd5ab15b12a37f0e9994d8847d5670da936a93d98672c442a956fab34ea0c15"}, - {file = "libcst-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5d5ba9314569865effd5baff3a58ceb2cced52228e181824759c68486a7ec8f4"}, - {file = "libcst-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2ec10015e86a4402c3d2084ede6c7c9268faea1ecb99592fe9e291c515aaa2"}, - {file = "libcst-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f6e693281d6e9a62414205fb300ec228ddc902ca9cb965a09f11561dc10aa94"}, - {file = "libcst-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e635eadb6043d5f967450af27125811c6ccc7eeb4d8c5fd4f1bece9d96418781"}, - {file = "libcst-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c568e14d29489f09faf4915af18235f805d5aa60fa194023b4fadf3209f0c94"}, - {file = "libcst-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9add619a825d6f176774110d79dc3137f353a236c1e3bcd6e063ca6d93d6e0ae"}, - {file = "libcst-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:57a6bcfc8ca8a0bb9e89a2dbf63ee8f0c7e8353a130528dcb47c9e59c2dc8c94"}, - {file = "libcst-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5e22738ec2855803f8242e6bf78057389d10f8954db34bf7079c82abab1b8b95"}, - {file = "libcst-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa519d4391326329f37860c2f2aaf80cb11a6122d14afa2f4f00dde6fcfa7ae4"}, - {file = "libcst-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b52692a28d0d958ebfabcf8bfce5fcf2c8582967310d35e6111a6e2d4db96659"}, - {file = "libcst-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61bfc90c8a4594296f8b68702f494dfdfec6e745a4abc0cfa8069d7f22061424"}, - {file = "libcst-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9370c23a3f609280c3f2296d61d34dd32afd7a1c9b19e4e29cc35cb2e2544363"}, - {file = "libcst-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e50e6960ecc3ed67f39fec63aa329e772d5d27f8e2334e30f19a94aa14489f1"}, - {file = "libcst-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ca4e91aa854758040fa6fe7036fbe7f90a36a7d283fa1df8587b6f73084fc997"}, - {file = "libcst-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d894c48f682b0061fdb2c983d5e64c30334db6ce0783560dbbb9df0163179c0c"}, - {file = "libcst-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:14e5c1d427c33d50df75be6bc999a7b2d7c6b7840e2361a18a6f354db50cb18e"}, - {file = "libcst-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93417d36c2a1b70d651d0e970ff73339e8dcd64d341672b68823fa0039665022"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6523731bfbdbc045ff8649130fe14a46b31ad6925f67acdc0e0d80a0c61719fd"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a252fa03ea00986f03100379f11e15d381103a09667900fb0fa2076cec19081a"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a5530b40a15dbe6fac842ef2ad87ad561760779380ccf3ade6850854d81406"}, - {file = "libcst-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0456381c939169c4f11caecdb30f7aca6f234640731f8f965849c1631930536b"}, - {file = "libcst-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8d6176a667d2db0132d133dad6bbf965f915f3071559342ca2cdbbec537ed12"}, - {file = "libcst-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:6137fe549bfbb017283c3cf85419eb0dfaa20a211ad6d525538a2494e248a84b"}, - {file = "libcst-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3923a341a787c1f454909e726a6213dd59c3db26c6e56d0a1fc4f2f7e96b45d7"}, - {file = "libcst-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d9a796c2f3d5b71dd06b7578e8d1fb1c031d2eb8d59e7b40e288752ae1b210"}, - {file = "libcst-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:932a4c4508bd4cf5248c99b7218bb86af97d87fefa2bdab7ea8a0c28c270724a"}, - {file = "libcst-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d12ffe199ff677a37abfb6b21aba1407eb02246dc7e6bcaf4f8e24a195ec4ad6"}, - {file = "libcst-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:81036e820249937608db7e72d0799180122d40d76d0c0414c454f8aa2ffa9c51"}, - {file = "libcst-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:94acd51ea1206460c20dea764c59222e62c45ae8a486f22024f063d11a7bca88"}, - {file = "libcst-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:c3445dce908fd4971ce9bb5fef5742e26c984027676e3dcf24875fbed1ff7e4c"}, - {file = "libcst-1.7.0.tar.gz", hash = "sha256:a63f44ffa81292f183656234c7f2848653ff45c17d867db83c9335119e28aafa"}, -] - -[package.dependencies] -pyyaml = ">=5.2" - -[package.extras] -dev = ["jupyter (>=1.0.0)", "libcst[dev-without-jupyter]", "nbsphinx (>=0.4.2)"] -dev-without-jupyter = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.2)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.5)", "maturin (>=1.7.0,<1.8)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools_scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] - [[package]] name = "mako" version = "1.3.9" @@ -938,31 +939,6 @@ babel = ["Babel"] lingua = ["lingua"] testing = ["pytest"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "3.0.2" @@ -1034,30 +1010,6 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "mock" version = "5.2.0" @@ -1140,6 +1092,18 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[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 = "packaging" version = "24.2" @@ -1186,18 +1150,6 @@ bcrypt = ["bcrypt (>=3.1.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] totp = ["cryptography"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.3.7" @@ -1231,6 +1183,25 @@ files = [ 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 = "prometheus-client" version = "0.21.1" @@ -1315,37 +1286,6 @@ files = [ {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] -[[package]] -name = "pycln" -version = "2.5.0" -description = "A formatter for finding and removing unused import statements." -optional = false -python-versions = "<4,>=3.8" -groups = ["dev"] -files = [ - {file = "pycln-2.5.0-py3-none-any.whl", hash = "sha256:6aec7a5b8df47e23399842b1f8470da4164956e26391f9b86c5edced5344da92"}, - {file = "pycln-2.5.0.tar.gz", hash = "sha256:f3a64486f813cd29da07940c4c2bb412080a23b9b0df9b0b1576c8e39ac79c44"}, -] - -[package.dependencies] -libcst = ">=0.3.10" -pathspec = ">=0.9.0" -pyyaml = ">=5.3.1" -tomlkit = ">=0.11.1" -typer = ">=0.4.1" - -[[package]] -name = "pycodestyle" -version = "2.12.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, -] - [[package]] name = "pydantic" version = "2.10.6" @@ -1365,7 +1305,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1501,25 +1441,13 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0 toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pygments" version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1724,25 +1652,6 @@ files = [ hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] -[[package]] -name = "rich" -version = "13.9.4" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "rsa" version = "4.9" @@ -1759,15 +1668,31 @@ files = [ pyasn1 = ">=0.1.3" [[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" +name = "ruff" +version = "0.11.3" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, + {file = "ruff-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:cb893a5eedff45071d52565300a20cd4ac088869e156b25e0971cb98c06f5dd7"}, + {file = "ruff-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58edd48af0e201e2f494789de80f5b2f2b46c9a2991a12ea031254865d5f6aa3"}, + {file = "ruff-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:520f6ade25cea98b2e5cb29eb0906f6a0339c6b8e28a024583b867f48295f1ed"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ca4405a93ebbc05e924358f872efceb1498c3d52a989ddf9476712a5480b16"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4341d38775a6be605ce7cd50e951b89de65cbd40acb0399f95b8e1524d604c8"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72bf5b49e4b546f4bea6c05448ab71919b09cf75363adf5e3bf5276124afd31c"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9fa791ee6c3629ba7f9ba2c8f2e76178b03f3eaefb920e426302115259819237"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c81d3fe718f4d303aaa4ccdcd0f43e23bb2127da3353635f718394ca9b26721"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4c38e9b6c01caaba46b6d8e732791f4c78389a9923319991d55b298017ce02"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9686f5d1a2b4c918b5a6e9876bfe7f47498a990076624d41f57d17aadd02a4dd"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4800ddc4764d42d8961ce4cb972bcf5cc2730d11cca3f11f240d9f7360460408"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e63a2808879361aa9597d88d86380d8fb934953ef91f5ff3dafe18d9cb0b1e14"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f8b1c4ae62638cc220df440140c21469232d8f2cb7f5059f395f7f48dcdb59e"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3ea2026be50f6b1fbedd2d1757d004e1e58bd0f414efa2a6fa01235468d4c82a"}, + {file = "ruff-0.11.3-py3-none-win32.whl", hash = "sha256:73d8b90d12674a0c6e98cd9e235f2dcad09d1a80e559a585eac994bb536917a3"}, + {file = "ruff-0.11.3-py3-none-win_amd64.whl", hash = "sha256:faf1bfb0a51fb3a82aa1112cb03658796acef978e37c7f807d3ecc50b52ecbf6"}, + {file = "ruff-0.11.3-py3-none-win_arm64.whl", hash = "sha256:67f8b68d7ab909f08af1fb601696925a89d65083ae2bb3ab286e572b5dc456aa"}, + {file = "ruff-0.11.3.tar.gz", hash = "sha256:8d5fcdb3bb359adc12b757ed832ee743993e7474b9de714bb9ea13c4a8458bf9"}, ] [[package]] @@ -1930,18 +1855,6 @@ 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 = "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 = "tornado" version = "6.4.2" @@ -1963,24 +1876,6 @@ files = [ {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] -[[package]] -name = "typer" -version = "0.15.2" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, - {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - [[package]] name = "types-mock" version = "5.2.0.20250306" @@ -2089,7 +1984,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +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 = "vine" @@ -2103,6 +1998,27 @@ files = [ {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, ] +[[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 = "wcwidth" version = "0.2.13" @@ -2135,5 +2051,5 @@ email = ["email-validator"] [metadata] lock-version = "2.1" -python-versions = "^3.13" -content-hash = "e8aa040e029d269df875ba419e5210a2f2ce4698e9fa7d985c2630f613c165f6" +python-versions = ">=3.13.0,<4.0.0" +content-hash = "1eb43dc6ddb28505f4a18228fb4118a2d00f0a9a2f2b8852b5e7270e498a0de2" diff --git a/pyproject.toml b/pyproject.toml index 8efaada..64421a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ version = "0.1.0" description = "Xmartlabs' Python project template" authors = [{ name = "Xmartlabs", email = "getintouch@xmartlabs.com" }] readme = "README.md" -requires-python = "^3.13" +requires-python = ">=3.13.0,<4.0.0" [tool.poetry] # TODO(remer): this can be removed when the source files are moved to project name folder within src @@ -13,7 +13,7 @@ requires-python = "^3.13" packages = [{ include = "*", from = "src" }] [tool.poetry.dependencies] -python = "^3.13" +python = ">=3.13.0,<4.0.0" alembic = "^1.15.1" asyncpg = "^0.30.0" @@ -36,16 +36,15 @@ sqlalchemy = "^2.0.39" uvicorn = "^0.34.0" [tool.poetry.group.dev.dependencies] -black = "^25.1.0" -flake8 = "^7.1.2" +coverage = "^7.7.1" flower = "^2.0.1" -isort = "^6.0.1" +mock = "^5.2.0" mypy = "^1.15.0" mypy-extensions = "^1.0.0" -pycln = "^2.5.0" pytest = "^8.3.5" pytest-asyncio = "0.26.0" -mock = "^5.2.0" +pre-commit = "^4.2.0" +ruff = "^0.11.3" [tool.poetry.group.types.dependencies] celery-types = "^0.23.0" @@ -60,6 +59,56 @@ typing-extensions = "^4.12.2" requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" +[tool.ruff] +line-length = 130 +force-exclude = true # Ensure exclusions are respected by the pre-commit hook +extend-exclude = ["src/alembic/versions", "__pycache__", "scripts"] + +[tool.ruff.lint] +extend-select = [ # Defaults: [ "E4", "E7", "E9", "F" ] (https://docs.astral.sh/ruff/rules/#error-e) + "E501", # line-too-long + "I001", # unsorted-imports + "I002", # missing-required-import +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.lint.isort] +known-first-party = ["src"] +known-third-party = ["fastapi", "sqlalchemy", "pydantic"] +force-single-line = false +combine-as-imports = true + +[tool.mypy] +plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"] +ignore_missing_imports = true +disallow_untyped_defs = true +warn_unused_ignores = false +no_implicit_optional = true +implicit_reexport = true +explicit_package_bases = true +namespace_packages = true +follow_imports = "silent" +warn_redundant_casts = true +check_untyped_defs = true +no_implicit_reexport = true +disable_error_code = ["name-defined", "call-arg", "attr-defined"] + +[[tool.mypy.overrides]] +module = "starlette_context.plugins" +implicit_reexport = true + +[[tool.mypy.overrides]] +module = "app.middlewares.logging_middleware" +warn_unused_ignores = false + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = false +warn_untyped_fields = true + [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" diff --git a/scripts/format.sh b/scripts/format.sh index 2087206..bb090f5 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,16 +1,10 @@ #!/bin/bash -printf "Runing pycln...\n" -poetry run python -m pycln src --exclude __init__.py --all - -printf "\nRunning isort...\n" -poetry run python -m isort src - -printf "\nRunning flake8...\n" -poetry run python -m flake8 src - printf "\nRunning mypy...\n" poetry run python -m mypy src -printf "\nRunning black...\n" -poetry run python -m black src --exclude alembic +printf "\nRunning ruff check...\n" +ruff check --fix + +printf "\nRunning ruff format...\n" +ruff format diff --git a/src/admin.py b/src/admin.py index e77a1c8..1efcdc7 100755 --- a/src/admin.py +++ b/src/admin.py @@ -31,9 +31,7 @@ async def logout(self, request: Request) -> bool: return True async def authenticate(self, request: Request) -> RedirectResponse | bool: - failed_auth_response = RedirectResponse( - request.url_for("admin:login"), status_code=302 - ) + failed_auth_response = RedirectResponse(request.url_for("admin:login"), status_code=302) manager = AuthManager() token = request.session.get(AdminAuth.cookie_name) if not token: diff --git a/src/alembic/env.py b/src/alembic/env.py index 5ba807d..041b4a7 100755 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -1,13 +1,14 @@ import asyncio from logging.config import fileConfig -from src import models # noqa F401 -from src.core.config import settings -from src.core.database import SQLBase -from sqlalchemy import pool + +from alembic import context +from sqlalchemy import engine_from_config, pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config -from alembic import context +from src import models # noqa F401 +from src.core.config import settings +from src.core.database import SQLBase # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/src/alembic/versions/2023-07-12-5d07fd610995_.py b/src/alembic/versions/2023-07-12-5d07fd610995_.py index a68bfe5..dff84a7 100755 --- a/src/alembic/versions/2023-07-12-5d07fd610995_.py +++ b/src/alembic/versions/2023-07-12-5d07fd610995_.py @@ -1,7 +1,7 @@ """empty message Revision ID: 5d07fd610995 -Revises: +Revises: Create Date: 2023-07-12 18:37:52.159059 """ diff --git a/src/api/dependencies.py b/src/api/dependencies.py index 8f5d330..7289ab8 100755 --- a/src/api/dependencies.py +++ b/src/api/dependencies.py @@ -19,8 +19,6 @@ async def db_session() -> AsyncIterator[AsyncSession]: await session.close() -async def get_user( - request: Request, session: AsyncSession = Depends(db_session) -) -> User: +async def get_user(request: Request, session: AsyncSession = Depends(db_session)) -> User: manager = AuthManager() return await manager(request=request, session=session) diff --git a/src/api/v1/router.py b/src/api/v1/router.py index bcff8c9..bd8eef5 100755 --- a/src/api/v1/router.py +++ b/src/api/v1/router.py @@ -5,6 +5,4 @@ v1_router = APIRouter() v1_router.include_router(user.router, prefix="/users") v1_router.include_router(item.router, prefix="/items") -v1_router.include_router( - task.router, tags=["Distributed Tasks Queue - Celery"], prefix="/tasks" -) +v1_router.include_router(task.router, tags=["Distributed Tasks Queue - Celery"], prefix="/tasks") diff --git a/src/api/v1/routers/item.py b/src/api/v1/routers/item.py index 53e723d..1c47584 100755 --- a/src/api/v1/routers/item.py +++ b/src/api/v1/routers/item.py @@ -14,9 +14,7 @@ @router.get("", response_model=Page[Item]) -async def get_items( - user: User = Depends(get_user), session: AsyncSession = Depends(db_session) -) -> Any: +async def get_items(user: User = Depends(get_user), session: AsyncSession = Depends(db_session)) -> Any: return await paginate(session, user.get_items()) @@ -26,9 +24,7 @@ async def create_item( user: User = Depends(get_user), session: AsyncSession = Depends(db_session), ) -> Any: - return await ItemController.create( - item_data=item_data, owner_id=user.id, session=session - ) + return await ItemController.create(item_data=item_data, owner_id=user.id, session=session) @router.post("/bulk", response_model=list[Item], status_code=201) @@ -38,6 +34,4 @@ async def create_item_async( session: AsyncSession = Depends(db_session), ) -> Any: """Create items asynchronously.""" - return await ItemController.bulk_create( - items_data=item_data.items, owner_id=user.id, async_session=session - ) + return await ItemController.bulk_create(items_data=item_data.items, owner_id=user.id, async_session=session) diff --git a/src/api/v1/routers/user.py b/src/api/v1/routers/user.py index 2c42b0c..a7f7b52 100755 --- a/src/api/v1/routers/user.py +++ b/src/api/v1/routers/user.py @@ -42,8 +42,6 @@ def me(user: models.User = Depends(get_user)) -> Any: @router.get("/{user_id}/items", response_model=Page[Item]) -async def get_public_items( - user_id: UUID, session: AsyncSession = Depends(db_session) -) -> Any: +async def get_public_items(user_id: UUID, session: AsyncSession = Depends(db_session)) -> Any: user = await models.User.objects(session).get_or_404(models.User.id == user_id) return await paginate(session, user.get_public_items()) diff --git a/src/api/v1/schemas/task.py b/src/api/v1/schemas/task.py index 5c49c90..9fe9231 100644 --- a/src/api/v1/schemas/task.py +++ b/src/api/v1/schemas/task.py @@ -10,13 +10,9 @@ class TaskCreate(BaseModel): class Task(BaseModel): - task_id: UUID = Field( - ..., description="Task ID", examples=["7ce6afd6-bc66-4db1-bf31-b99e6daa0f11"] - ) + task_id: UUID = Field(..., description="Task ID", examples=["7ce6afd6-bc66-4db1-bf31-b99e6daa0f11"]) class TaskResult(Task): - task_status: str = Field( - ..., description="Task status", examples=["SUCCESS", "FAILURE"] - ) + task_status: str = Field(..., description="Task status", examples=["SUCCESS", "FAILURE"]) task_result: int | None = Field(None, description="Task result", examples=[6, None]) diff --git a/src/controllers/item.py b/src/controllers/item.py index 45d8dbb..ebe96eb 100755 --- a/src/controllers/item.py +++ b/src/controllers/item.py @@ -8,9 +8,7 @@ class ItemController: @staticmethod - async def create( - item_data: schemas.ItemCreate, owner_id: UUID, session: AsyncSession - ) -> models.Item: + async def create(item_data: schemas.ItemCreate, owner_id: UUID, session: AsyncSession) -> models.Item: item_data = schemas.Item(owner_id=owner_id, **item_data.model_dump()) item = await models.Item.objects(session).create(item_data.model_dump()) await session.refresh(item) @@ -22,13 +20,8 @@ async def bulk_create( owner_id: UUID, async_session: AsyncSession, ) -> Sequence[models.Item]: - items_data = [ - schemas.Item(owner_id=owner_id, **item_data.model_dump()) - for item_data in items_data - ] - items = await models.Item.objects(async_session).bulk_create( - [item_data.model_dump() for item_data in items_data] - ) + items_data = [schemas.Item(owner_id=owner_id, **item_data.model_dump()) for item_data in items_data] + items = await models.Item.objects(async_session).bulk_create([item_data.model_dump() for item_data in items_data]) for item in items: await async_session.refresh(item) diff --git a/src/controllers/user.py b/src/controllers/user.py index 950d3f5..d505bd6 100755 --- a/src/controllers/user.py +++ b/src/controllers/user.py @@ -20,9 +20,7 @@ async def create(user_data: UserCreate, session: AsyncSession) -> User: @staticmethod async def login(user_data: UserCreate, session: AsyncSession) -> User: - login_exception = HTTPException( - status_code=401, detail="Invalid email or password" - ) + login_exception = HTTPException(status_code=401, detail="Invalid email or password") user = await User.objects(session).get(User.email == user_data.email) if not user: raise login_exception diff --git a/src/core/database.py b/src/core/database.py index d7031b3..79c0001 100755 --- a/src/core/database.py +++ b/src/core/database.py @@ -50,9 +50,7 @@ def _on_handle_error(context: ExceptionContext) -> None: Returns: None: this returns nothing. """ - logging.warning( - f"handle_error event triggered for PostgreSQL engine: {context.sqlalchemy_exception}" - ) + logging.warning(f"handle_error event triggered for PostgreSQL engine: {context.sqlalchemy_exception}") if "Can't connect to PostgreSQL server on" in str(context.sqlalchemy_exception): # Setting is_disconnect to True should tell SQLAlchemy treat this as a connection error and retry context.is_disconnect = True # type: ignore @@ -61,9 +59,7 @@ def _on_handle_error(context: ExceptionContext) -> None: def async_session_generator() -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - autocommit=False, autoflush=False, bind=engine, class_=AsyncSession - ) + return async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession) class SQLBase(AsyncAttrs, DeclarativeBase): @@ -113,9 +109,7 @@ async def get(self, *where_clause: Any) -> _Model | None: async def get_or_404(self, *where_clause: Any) -> _Model: obj = await self.get(*where_clause) if obj is None: - raise HTTPException( - status_code=404, detail=f"{self.cls.__name__} not found" - ) + raise HTTPException(status_code=404, detail=f"{self.cls.__name__} not found") return obj async def get_all(self, *where_clause: Any) -> Sequence[_Model]: @@ -149,14 +143,10 @@ async def bulk_create(self, data: Sequence[Dict[str, Any]]) -> Sequence[_Model]: @declarative_mixin class TableIdMixin: - id: Mapped[uuid.UUID] = mapped_column( - primary_key=True, server_default=random_uuid() - ) + id: Mapped[uuid.UUID] = mapped_column(primary_key=True, server_default=random_uuid()) @declarative_mixin class DatedTableMixin(TableIdMixin): created_at: Mapped[datetime] = mapped_column(server_default=utcnow()) - updated_at: Mapped[datetime] = mapped_column( - server_default=utcnow(), onupdate=datetime.now(timezone.utc) - ) + updated_at: Mapped[datetime] = mapped_column(server_default=utcnow(), onupdate=datetime.now(timezone.utc)) diff --git a/src/core/security.py b/src/core/security.py index 219b4cb..d8f9341 100755 --- a/src/core/security.py +++ b/src/core/security.py @@ -38,16 +38,10 @@ class AuthManager: accept_header = settings.accept_token @classmethod - def create_access_token( - cls, user: User, expires_delta: timedelta | None = None - ) -> Tuple[str, datetime]: - expires = datetime.now(timezone.utc) + ( - expires_delta or timedelta(minutes=settings.access_token_expire_minutes) - ) + def create_access_token(cls, user: User, expires_delta: timedelta | None = None) -> Tuple[str, datetime]: + expires = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes)) claims = {"exp": expires, "user_id": str(user.id)} - token = jwt.encode( - claims=claims, key=settings.jwt_signing_key, algorithm=cls.algorithm - ) + token = jwt.encode(claims=claims, key=settings.jwt_signing_key, algorithm=cls.algorithm) return token, expires @classmethod @@ -65,9 +59,7 @@ def process_login(cls, user: User, response: Response) -> Token | None: async def get_user_from_token(self, token: str, session: AsyncSession) -> User: try: - payload = jwt.decode( - token=token, key=settings.jwt_signing_key, algorithms=self.algorithm - ) + payload = jwt.decode(token=token, key=settings.jwt_signing_key, algorithms=self.algorithm) token_data = TokenPayload(**payload) except (JWTError, ValidationError): raise self.credentials_exception diff --git a/src/tests/base.py b/src/tests/base.py index 3dcd891..b43601d 100755 --- a/src/tests/base.py +++ b/src/tests/base.py @@ -21,9 +21,7 @@ def async_session_generator() -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession - ) + return async_sessionmaker(autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession) async def override_get_db() -> AsyncIterator[AsyncSession]: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 59b412a..5fb4ae8 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -22,9 +22,7 @@ async def reset_database() -> AsyncGenerator: @pytest.fixture async def async_client() -> AsyncGenerator: - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as ac: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: yield ac diff --git a/src/tests/test_user.py b/src/tests/test_user.py index fc55f25..91a0259 100755 --- a/src/tests/test_user.py +++ b/src/tests/test_user.py @@ -50,12 +50,8 @@ def check_me_response(self, response: Response) -> None: assert data["is_active"] == True # noqa: E712 assert data["is_superuser"] == False # noqa: E712 - async def test_signup( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: - expected_expire = datetime.now(timezone.utc) + timedelta( - minutes=settings.access_token_expire_minutes - ) + async def test_signup(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: + expected_expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) response = await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) assert response.status_code == 201 @@ -70,22 +66,16 @@ async def test_signup_wrong_shema(self, async_client: AsyncClient) -> None: response = await async_client.post(self.SIGNUP_URL) assert response.status_code == 422 - async def test_signup_dup_emails( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_signup_dup_emails(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) response = await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) assert response.status_code == 409 data = response.json() assert data["detail"] == "Email address already in use" - async def test_login( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_login(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) - expected_expire = datetime.now(timezone.utc) + timedelta( - minutes=settings.access_token_expire_minutes - ) + expected_expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) response = await async_client.post(self.LOGIN_URL, json=self.TEST_PAYLOAD) assert response.status_code == 200 data = response.json() @@ -99,58 +89,40 @@ async def test_login_wrong_shema(self, async_client: AsyncClient) -> None: response = await async_client.post(self.LOGIN_URL) assert response.status_code == 422 - async def test_login_fail( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_login_fail(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: response = await async_client.post(self.LOGIN_URL, json=self.TEST_PAYLOAD) self.check_login_fail(response=response) - async def test_login_bad_password( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_login_bad_password(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) payload = {"email": self.TEST_EMAIL, "password": "oops"} response = await async_client.post(self.LOGIN_URL, json=payload) self.check_login_fail(response=response) - async def test_me_header( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_header(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: sign_up_resp = await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) data = sign_up_resp.json() token = f"Bearer {data['access_token']}" async_client.cookies.clear() - response = await async_client.get( - self.ME_URL, headers={AuthManager.header_name: token} - ) + response = await async_client.get(self.ME_URL, headers={AuthManager.header_name: token}) self.check_me_response(response=response) - async def test_me_cookie( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_cookie(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) response = await async_client.get(self.ME_URL) self.check_me_response(response=response) - async def test_me_unauthenticated( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_unauthenticated(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: response = await async_client.get(self.ME_URL) assert response.status_code == 401 - async def test_me_bad_access_token( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_bad_access_token(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) async_client.cookies.clear() - response = await async_client.get( - self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN} - ) + response = await async_client.get(self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN}) assert response.status_code == 401 - async def test_me_bad_cookie( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_bad_cookie(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) async_client.cookies.clear() async_client.cookies.set(name=AuthManager.cookie_name, value=self.BAD_TOKEN) @@ -158,9 +130,7 @@ async def test_me_bad_cookie( assert response.status_code == 401 @patch.object(settings, "access_token_expire_minutes", 0.02) - async def test_expired_token( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_expired_token(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) time.sleep(3) response = await async_client.get(self.ME_URL)