diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..420d96e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +**/.venv +**/__pycache__ +**/.pytest_cache +**/.ruff_cache +**/*.egg-info +**/dist +**/build +**/.env +**/.env.* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9845888 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a85eff8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto +*.sh text eol=lf +Makefile text eol=lf +*.py text eol=lf +*.toml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..852d797 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 +updates: + - package-ecosystem: uv + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + development-tools: + patterns: ["pytest*", "ruff", "ty", "pre-commit"] + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] + - package-ecosystem: docker + directories: + - /templates/service + - /services/* + schedule: + interval: weekly + groups: + container-images: + patterns: ["*"] diff --git a/.github/workflows/test_all_services_and_packages.yaml b/.github/workflows/test_all_services_and_packages.yaml index 6b241ed..5217205 100644 --- a/.github/workflows/test_all_services_and_packages.yaml +++ b/.github/workflows/test_all_services_and_packages.yaml @@ -1,25 +1,69 @@ -name: Test All Services and Packages +name: CI on: pull_request: - branches: [ main ] push: -# branches: [ main ] + branches: [main] workflow_dispatch: +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: - test: + check: runs-on: ubuntu-latest - + timeout-minutes: 15 strategy: + fail-fast: false matrix: - folder: [services/*, packages/*] - + python-version: ["3.12", "3.13", "3.14", "3.15"] steps: - - name: Checkout code - uses: actions/checkout@v2 + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: "0.13.4" + python-version: ${{ matrix.python-version }} + enable-cache: true + - run: uv sync --all-packages --locked + - run: make lint + - name: Check formatting + if: ${{ !cancelled() }} + run: uv run --locked ruff format --check --diff . + - name: Check types + if: ${{ !cancelled() }} + run: make typecheck + - name: Run tests + if: ${{ !cancelled() }} + run: make test - - name: Test ${{ matrix.folder }} - uses: ./.github/actions/test-service-or-package + smoke: + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + arguments: --docker + - os: macos-latest + arguments: "" + - os: windows-latest + arguments: "" + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - target_path: ${{ matrix.folder }} + version: "0.13.4" + python-version: "3.14" + enable-cache: true + - run: uv sync --locked + - name: Generate, test, build, and install workspace members + run: uv run --locked python scripts/smoke_test.py ${{ matrix.arguments }} diff --git a/.github/workflows/test_service_or_package.yaml b/.github/workflows/test_service_or_package.yaml index 351465c..0737a77 100644 --- a/.github/workflows/test_service_or_package.yaml +++ b/.github/workflows/test_service_or_package.yaml @@ -1,41 +1,42 @@ -name: Validate Code +name: Validate One Workspace Member on: workflow_dispatch: inputs: target_path: - description: 'The path of the package or service to validate' + description: "Workspace member path, for example services/my-service" required: true + type: string + +permissions: + contents: read jobs: - validate_code: + validate: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11"] + timeout-minutes: 15 steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Poetry - run: pip install poetry - - - name: Install Dependencies - run: poetry install - working-directory: ${{ github.workspace }}/${{ github.event.inputs.target_path }} - - - name: Lint - run: make -f Makefile lint - working-directory: ${{ github.workspace }}/${{ github.event.inputs.target_path }} - - - name: Format - run: make -f Makefile format args="--check" - working-directory: ${{ github.workspace }}/${{ github.event.inputs.target_path }} - - - name: Test - run: make -f Makefile test - working-directory: ${{ github.workspace }}/${{ github.event.inputs.target_path }} + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: "0.13.4" + python-version: "3.14" + enable-cache: true + - run: uv sync --all-packages --locked + - name: Validate selected member + env: + TARGET_PATH: ${{ inputs.target_path }} + shell: bash + run: | + set -euo pipefail + if [[ ! "$TARGET_PATH" =~ ^(services|packages)/[a-z][a-z0-9_-]*$ ]] || [[ ! -f "$TARGET_PATH/pyproject.toml" ]]; then + echo "Expected an existing services/ or packages/ workspace member" >&2 + exit 1 + fi + cd "$TARGET_PATH" + uv run --locked ruff check src tests + uv run --locked ruff format --check src tests + uv run --locked ty check src tests + uv run --locked pytest tests diff --git a/.gitignore b/.gitignore index ea7dd47..0c995a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,170 +1,17 @@ -### Python template -# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python +*.egg-info/ +.venv/ 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 +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.coverage* 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 - -# 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/#use-with-ide -.pdm.toml - -# 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. +.env.* +!.env.example .idea/ - -# ruff cache -**/.ruff-cache/ - -# pytest cache -**/.pytest_cache/ - -# TODO files -**/TODO +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14cc7a8..1a6fdd7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,19 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + - repo: local hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - -- repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - # It is recommended to specify the latest version of Python - # supported by your project here, or alternatively use - # pre-commit's default_language_version, see - # https://pre-commit.com/#top_level-default_language_version - language_version: python3.11 - -- repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.261' - hooks: - - id: ruff + - id: ruff-check + name: Ruff lint + entry: uv run --locked ruff check + language: system + types_or: [python, pyi] + - id: ruff-format + name: Ruff format + entry: uv run --locked ruff format --check + language: system + types_or: [python, pyi] + - id: ty-check + name: Type check workspace + entry: uv run --all-packages --locked ty check + language: system + pass_filenames: false + types_or: [python, pyi] diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Makefile b/Makefile index 60ebf02..224cc00 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,33 @@ -new-service: ## Create a new service - @bash scripts/new-service.sh $(name) +.DEFAULT_GOAL := help +export name +.PHONY: help install typecheck smoke new-service new-package format lint test check -new-package: ## Create a new package - @bash scripts/new-package.sh $(name) +install: ## Install all workspace members and development tools + uv sync --all-packages --locked -.PHONY: help -help: - @awk 'BEGIN {FS = ":.*##"; printf "Usage: make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +new-service: ## Create a service: make new-service name=my-service + @python3 scripts/scaffold.py service "$$name" + +new-package: ## Create a package: make new-package name=my-package + @python3 scripts/scaffold.py package "$$name" + +format: ## Format Python code + uv run --locked ruff format . $(args) + +lint: ## Lint Python code + uv run --locked ruff check . $(args) + +test: ## Test scaffolding and every workspace member + uv run --all-packages --locked python scripts/test_all.py $(args) + +typecheck: ## Check types across scripts, tests, and workspace members + uv run --all-packages --locked ty check + +smoke: ## Generate, test, build, and install connected sample projects + uv run --locked python scripts/smoke_test.py $(args) + +check: lint typecheck test ## Run lint, type, formatting, and test checks + uv run --locked ruff format --check . + +help: ## Show available commands + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index b6ff27e..67d8f38 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,236 @@ -# Python Mono-Repo Template -This is a template for creating a Python mono-repository that allows storing multiple separate services and packages -that can be dependencies of these services. The template serves as a skeleton that allows easy creation of new services -and packages, and takes care of different aspects such as code linting, unit testing, and building Docker images. - -### Features: -* 🐍 [Monorepo](https://en.wikipedia.org/wiki/Monorepo)-ready: multiple separate services and packages in a single repository -* πŸ“¦ [Poetry](https://github.com/python-poetry/poetry) for dependency management -* πŸ“ Easy creation of new services and packages with a Makefile and templates -* πŸ–‹οΈ Python code formatting using [Black](https://github.com/psf/black) (The uncompromising Python code formatter) -* ⚑ Code linting using [Ruff](https://github.com/charliermarsh/ruff) (An extremely fast Python linter, written in Rust.) -* 🐳 Preconfigured Dockerfiles for services -* πŸͺ [Pre-commit](https://github.com/pre-commit/pre-commit) hooks for linting and formatting -* πŸš€ GitHub Actions for linting and testing +
---- +# 🐍 Python Monorepo Template -## Features -#### 🐍 Templates for Python packages and services: -This template provides a set of templates for creating new Python -packages and services. These templates include boilerplate code and configuration files that make it easy to get started -and ensure consistency across all the components in your repository. +**A home for your Python services and the code they share.** +Start with a service, add a shared library, and keep them together in one repo.
+Dependency management, tests, formatting, and Docker builds are ready to go. -#### πŸ“¦ Dependency management with Poetry: -This repository uses Poetry as the preferred way to track dependencies for -Python packages and services. Poetry provides a simple and reliable way to manage dependencies, and is recommended over -the old setup.py files and requirements.txt files. With Poetry, you can easily add and remove dependencies, generate -lockfiles for reproducible builds, and manage virtual environments for different projects. +[![Python 3.12+](https://img.shields.io/badge/Python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) +[![uv workspace](https://img.shields.io/badge/workspace-uv-DE5FE9)](https://docs.astral.sh/uv/) +[![Ruff + ty](https://img.shields.io/badge/code_checks-Ruff_%2B_ty-D7FF64?labelColor=263238)](#everyday-workflow) +[![MIT license](https://img.shields.io/badge/license-MIT-22C55E)](LICENSE) +[**Use this template β†’**](https://github.com/matanby/python-monorepo-template/generate) -#### πŸ–‹οΈ Python code formatting using Black: -This project uses Black, a Python code formatter, to ensure that code is consistently formatted and adheres to -PEP 8 guidelines. Black takes care of automatic code formatting, saving you time and energy that would be spent on -manual formatting. +[Quick start](#quick-start) Β· [Everyday workflow](#everyday-workflow) Β· [Project map](#project-map) Β· [Docker](#docker) Β· [Migration](#migration) +
-#### ⚑️Code linting using Ruff: -This project uses Ruff, a new super fast Python linter written in Rust, to check code for syntax errors, style -violations, and potential bugs. Ruff is designed to be extremely fast, making it a good choice for large codebases. +--- +## What you get + +- 🐍 **Modern Python.** Python 3.14 by default, with CI checks for 3.12–3.15. +- πŸ—‚οΈ **One repo, many projects.** Keep your services and shared libraries together. +- ⚑ **Fast setup with uv.** Manage dependencies and virtual environments in one place. +- πŸ”’ **One shared lockfile.** Give everyone the same dependency versions. +- πŸš€ **One-command scaffolding.** Create a service or library with source, tests, and docs. +- 🧩 **Easy code sharing.** Use local libraries without rebuilding them after every edit. +- ✨ **Clean code with Ruff.** Format your Python and catch lint errors. +- πŸ”Ž **Type checks with ty.** Find type errors while you work. +- πŸ§ͺ **Tests with pytest.** Run each project's tests independently. +- πŸͺ **Optional commit hooks.** Run checks before your changes leave your laptop. +- 🐳 **Docker included.** Build service images with a non-root production runtime. +- πŸ“¦ **Ready to package.** Build wheels with type markers and license metadata. +- 🌍 **Cross-platform smoke tests.** Check generated projects on Linux, macOS, and Windows. +- πŸ€– **Dependency updates.** Get weekly Dependabot PRs for Python, Actions, and Docker. +- πŸ› οΈ **Commands you'll remember.** Use Make shortcuts or run uv directly. + +## Quick start + +Let's get your first service running. πŸš€ + +[Create a repo from this template](https://github.com/matanby/python-monorepo-template/generate), +clone it, and open a terminal at its root. You'll need +[uv](https://docs.astral.sh/uv/getting-started/installation/) 0.11+, GNU Make, and +Python 3.12+ available as `python3`. See [Working without Make](#working-without-make) +for direct commands, including on Windows. + +```sh +# Set up the workspace (uv downloads Python 3.14 if needed) +uv sync --all-packages --locked + +# Create a library and a service that can use it +make new-package name=shared-utils +make new-service name=my-service +uv add --package my-service --workspace shared-utils +uv sync --all-packages --locked + +# Say hello +make -C services/my-service run +``` -#### 🐳 Preconfigured Dockerfiles: -This template includes a basic Dockerfile that can be used to build and run services in containers. -This makes it easy to deploy your services to different environments and helps ensure that your code runs consistently -across different machines.* +```text +Hello from my-service! +``` +Your library imports as `shared_utils`. Start adding code, then run `make check` +to check lint, types, tests, and formatting. Commit your new projects along with +`uv.lock` so everyone gets the same dependencies. -#### πŸͺ Pre-commit hooks for linting and formatting: -This template comes with pre-commit hooks that automatically check -your code for formatting errors, style issues, and other problems before you commit changes to Git. This helps ensure -that your code is consistent and adheres to the project's standards. +
+A few things to know about new projects +Names start with a lowercase letter and may contain lowercase letters, digits, +hyphens, or underscores. `shared-utils` becomes `packages/shared-utils` with an +import name of `shared_utils`. Names must be unique across services and libraries; +Python keywords and standard-library module names are rejected. -#### πŸš€ GitHub Actions for linting and testing: -This template includes GitHub Actions that automatically run linting and -testing scripts whenever you push changes to your repository. This helps catch errors and issues early on, before they -can cause problems in production. +The generator works offline. After creating a project, run `uv lock` and +`uv sync --all-packages --locked` (adding a dependency with `uv add` also updates +the lockfile). Each project uses a `src/` layout and includes license metadata +and a `py.typed` marker for tools that use its type annotations. +
-#### πŸ› οΈ Makefile for running common commands: -This template includes a Makefile that provides a set of common commands for -working with the repository, such as running tests, building Docker images, and cleaning up temporary files. +## Everyday workflow ---- +Run these at the repo root, or use `make -C ` for a single project. -## Getting Started -To use this template, simply clone the repository and start creating new services and packages based on the provided -templates. You can customize the pre-commit hooks, GitHub Actions, and other configuration files to meet your specific needs. +| Command | What it does | +| --- | --- | +| `make install` | Install all projects and the locked development tools | +| `make format` | Format your Python code | +| `make check` | Run lint, types, tests, and a formatting check | +| `make lint` / `make typecheck` / `make test` | Run just one kind of check | +| `make -C packages/shared-utils test` | Test a single project | +| `make smoke` | Generate sample projects, build wheels, and test a fresh install | +| `uv build --package shared-utils` | Build a library wheel and source archive | +| `uv run --locked pre-commit install` | Enable commit hooks | +| `uv lock --upgrade` | Update dependencies for review | +Use `make help` to see the available commands. You can pass arguments too: +`make -C services/my-service test args="-q"`. -### File Structure -``` -β”œβ”€β”€ doc # Project-level documentation folder -β”œβ”€β”€ packages # Packages root folder -β”œβ”€β”€ scripts # Project-level scripts folder -β”œβ”€β”€ services # Services root folder -β”œβ”€β”€ templates # Template folder for new packages and services -β”‚Β Β  β”œβ”€β”€ package # Package template folder -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ doc # Package documentation folder -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ scripts # Package scripts folder -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ src # Package source code folder -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ tests # Package test folder -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ Makefile # Package Makefile for running common commands -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ README.md # Package README file -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ poetry.lock # Poetry lock file for package dependencies -β”‚Β Β  β”‚Β Β  └── pyproject.toml # Poetry configuration file for the package -β”‚Β Β  └── service # Service template folder -β”‚Β Β  β”œβ”€β”€ doc # Service documentation folder -β”‚Β Β  β”œβ”€β”€ scripts # Service scripts folder -β”‚Β Β  β”œβ”€β”€ src # Service source code folder -β”‚Β Β  β”œβ”€β”€ tests # Service test folder -β”‚Β Β  β”œβ”€β”€ Dockerfile # Dockerfile for building the service -β”‚Β Β  β”œβ”€β”€ Makefile # Service Makefile for running common commands -β”‚Β Β  β”œβ”€β”€ README.md # Service README file -β”‚Β Β  β”œβ”€β”€ poetry.lock # Poetry lock file for service dependencies -β”‚Β Β  └── pyproject.toml # Poetry configuration file for the service -β”œβ”€β”€ LICENSE # License file -β”œβ”€β”€ Makefile # Project-level Makefile for running common commands for the entire repo -β”œβ”€β”€ README.md # README file for the entire repo -└── pyproject.toml # Poetry configuration file for the entire repo +### Add a dependency + +```sh +uv add --package my-service httpx # For one service +uv add --package my-service --workspace shared-utils # A library from this repo +uv add --dev pytest-cov # A development tool +uv sync --all-packages --locked ``` -### Creating New Services and Packages -This repository includes a Makefile that provides convenient targets for creating new services and packages. -These targets are implemented using shell scripts located in the scripts directory, and provide boilerplate code and -configuration files to help get started with new projects. +Local libraries are installed in editable mode, so your service can pick up source +changes without rebuilding a wheel. All projects share one dependency resolution +and need compatible requirements; use separate environments when they don't. -To create a new service, run the new-service target from the command line and provide a name for your service: +## Project map -```shell -make new-service name= +```mermaid +flowchart LR + subgraph workspace["Your repo Β· one uv workspace"] + service["services/my-service
Runnable Python service"] + library["packages/shared-utils
Reusable Python library"] + service -->|depends on| library + end + service -->|build| image["Docker image
Production dependencies"] ``` -This will create a new service directory in the services directory with the provided name. -The directory will contain a Dockerfile for building the service, a pyproject.toml file for listing dependencies, -and a basic project structure. +```text +. +β”œβ”€β”€ packages/ Shared libraries, each with src/ and tests/ +β”œβ”€β”€ services/ Runnable services, each with a Dockerfile +β”œβ”€β”€ templates/ Starting points for new projects +β”œβ”€β”€ scripts/ Scaffolding, test runner, and smoke checks +β”œβ”€β”€ tests/ Tests for the scaffolder +β”œβ”€β”€ .github/ CI and Dependabot configuration +β”œβ”€β”€ doc/ Your project documentation +β”œβ”€β”€ pyproject.toml Workspace and tool configuration +β”œβ”€β”€ uv.lock Shared dependency lockfile +└── .python-version Default Python version +``` + +Templates aren't installed as workspace projects. Tests run separately per project, +so matching test filenames won't clash. Ruff settings come from the root config. + +## Docker -To create a new package, run the new-package target from the command line and provide a name for your package: +Your service gets a Dockerfile too. 🐳 After updating the lockfile, build and run it: -```shell -make new-package name= +```sh +make -C services/my-service docker-build +make -C services/my-service docker-run ``` -This will create a new Python package in the packages directory with the provided name. -The directory will contain a basic project structure with a pyproject.toml file for listing dependencies. +Builds use the repo root as their context, including any shared libraries your +service needs. The runtime image contains the service and its production +dependencies and runs as user **10001**. -These targets are designed to help you get started quickly and provide a consistent project structure across all your -services and packages. You can modify the shell scripts to suit your own needs or create your own targets in the -Makefile as needed. +Keep secrets outside the build context; `.dockerignore` excludes environment files, +Git data, and local caches. Behind an HTTPS proxy, pass a combined CA bundle with +`args="--secret id=proxy_ca,src=/path/to/ca.pem"`. Update the pinned uv versions in +Docker and CI together. ---- +## What CI checks + +Pull requests and pushes to `main` check locked installation, lint, types, +formatting, and tests on **Python 3.12–3.15**. -## Using the Makefile +Smoke tests on **Linux, macOS, and Windows** create connected projects, run their +checks, build wheels, and install them in a fresh environment. Linux also builds +and runs the Docker image. Try it locally with `make smoke`, or +`make smoke args="--docker"` to include Docker. These checks need network access; +the Docker check also needs Docker running. -This repository includes a Makefile with targets that automate common tasks for Python services. -These targets are implemented using shell scripts located in the scripts directory and can be invoked from the command -line using the make command. +A manual workflow lets you check one existing service or library. Dependabot +opens weekly updates for Python dependencies, GitHub Actions, and Docker images. -Here are the available targets: +## Working without Make -### Installation -This target installs the service and its dependencies into a virtual environment. -It uses Poetry to manage dependencies and installs them according to the configuration in the pyproject.toml file. +
+Use uv directly on Windows, macOS, or Linux -To use this target, run the following command from the root directory of your service: -```shell -make install +```sh +uv sync --all-packages --locked +uv run --locked python scripts/scaffold.py service my-service +uv lock +uv sync --all-packages --locked +uv run --all-packages --locked python scripts/test_all.py +uv run --all-packages --locked ty check +uv run --locked ruff check . +uv run --locked ruff format --check . +uv run --locked python scripts/smoke_test.py ``` -### Code formatting -This target formats the code using Black, a Python code formatter. It applies a consistent coding style to your -codebase and ensures that it follows the PEP 8 guidelines. +For a library, replace `service my-service` with `package shared-utils`. -To use this target, run the following command from the root directory of your service: +The scaffolder's own tests also run without installing development tools: +`python -m unittest discover -s tests -v` with Python 3.12+. -```shell -make format -``` +
-### Linting -This target lints the code using Ruff, a fast Python linter written in Rust. It checks your codebase for syntax errors, -style violations, and potential bugs, and provides a report of any issues found. +## Migration -To use this target, run the following command from the root directory of your service: -```shell -make lint -``` +
+Updating an older Poetry-based copy -### Unit testing -This target runs the unit tests for your service using Pytest, a Python testing framework. It discovers and runs all -tests in the tests directory and provides a report of the test results. +1. Move Poetry metadata to `[project]` and development dependencies to + `[dependency-groups]`. +2. Add local workspace dependencies with `uv add --workspace`. +3. Run `uv lock`, `uv sync --all-packages`, and `make check`. +4. Once you've verified the conversion, remove the Poetry lockfiles and commit `uv.lock`. +5. Update service launch commands: services now use `src//__main__.py` + and `[project.scripts]` instead of `src/run.py`. -To use this target, run the following command from the root directory of your service: -```shell -make test -``` +Ruff replaces Black, so expect a one-time formatting diff. Commit changes to +`.python-version` when changing the default Python version. -## Contributing -If you find any issues with this template or have suggestions for improvements, please feel free to contribute to the -project. You can open issues, submit pull requests, or contact the project maintainers directly. +
+ +## Built on + +[uv](https://docs.astral.sh/uv/) Β· [Ruff](https://docs.astral.sh/ruff/) Β· +[ty](https://docs.astral.sh/ty/) Β· [pytest](https://docs.pytest.org/) Β· +[Hatchling](https://hatch.pypa.io/) Β· [pre-commit](https://pre-commit.com/) + +Based on uv's [workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) +and [Docker](https://docs.astral.sh/uv/guides/integration/docker/) guides, with +[uv-docker-example](https://github.com/astral-sh/uv-docker-example) and +[Substrate](https://github.com/superlinear-ai/substrate) as references. + +--- -## License -This project is licensed under the MIT License - see the LICENSE file for details. +[MIT licensed](LICENSE) Β· Make yourself at home. diff --git a/pyproject.toml b/pyproject.toml index f654264..63fee52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,48 +1,37 @@ -[tool.poetry] +[project] name = "python-monorepo-template" version = "0.1.0" -description = "" -authors = [] -readme = "README.md" +description = "A uv workspace for Python services and shared packages" +requires-python = ">=3.12" -[tool.poetry.dependencies] -python = "^3.9" +[tool.uv] +package = false +required-version = ">=0.11" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.uv.workspace] +members = ["packages/*", "services/*"] -[tool.black] -line-length = 100 +[dependency-groups] +dev = [ + "pre-commit>=4,<5", + "pytest>=8,<10", + "ruff>=0.15,<1", + "ty>=0.0.40,<0.1", +] [tool.ruff] -select = [ - "E", # pycodestyle - "F", # pyflakes - "W", # pycodestyle (warnings) - "I", # isort - "N", # pep8-naming - "UP", # pyupgrade - "ANN", # flake8-annotations - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "EXE", # flake8-executable - "PIE", # flake8-pie - "T20", # flake8-print - "PT", # flake8-pytest - "SIM", # flake8-simplify - "ARG", # flake8-unused-arguments - "PTH", # flake8--use-pathlib - "ERA", # flake8-eradicate - "RUF", # ruff specific rules - "PL", # pylint -] -ignore = [ - "ANN101", # Missing type annotation for self in method - "ANN204", # Missing type annotation for special method -] line-length = 100 -target-version = "py311" +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP", "B", "SIM", "C4", "PTH", "RUF"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --import-mode=importlib" + +[tool.ty.environment] +python-version = "3.12" + +[tool.ty.src] +exclude = ["templates"] diff --git a/scripts/new-package.sh b/scripts/new-package.sh index 8b6a0de..afa549a 100644 --- a/scripts/new-package.sh +++ b/scripts/new-package.sh @@ -1,48 +1,3 @@ #!/usr/bin/env bash - -# Change folder to the root of the project -cd "$(dirname "$0")/.." - -# Get the package name from the first argument -PACKAGE_NAME=$1 - -# If package name is empty, exit -if [ -z "$PACKAGE_NAME" ]; then - echo "Package name is required" - exit 1 -fi - -# Validate the package name, must be alphanumeric, non empty, and no spaces. Dashes is allowed. -if [[ ! $PACKAGE_NAME =~ ^[a-zA-Z0-9-]+$ ]]; then - echo "Invalid package name: $PACKAGE_NAME" - echo "Package name must be alphanumeric, non empty, and no spaces. Dashes is allowed." - exit 1 -fi - -# If package already exists, exit -if [ -d "packages/$PACKAGE_NAME" ]; then - echo "Package already exists: $PACKAGE_NAME" - exit 1 -fi - -# Create a copy of the package name, while replacing dashes with underscores -PACKAGE_NAME_UNDERSCORES=$(echo $PACKAGE_NAME | sed 's/-/_/g') - -# Set the package folder -PACKAGE_FOLDER=packages/$PACKAGE_NAME_UNDERSCORES - -# Copy the package template to the packages directory -cp -r templates/package $PACKAGE_FOLDER - -# Rename the package template files to the package name -mv $PACKAGE_FOLDER/src/package_name $PACKAGE_FOLDER/src/$PACKAGE_NAME_UNDERSCORES - -export LC_ALL=C -# Replace all instances of "" with the package name, in all files in the package directory -find $PACKAGE_FOLDER -type f -exec sed -i '' "s//$PACKAGE_NAME_UNDERSCORES/g" {} \; - -# Replace all instances of "" with the package name, in all files in the package directory -find $PACKAGE_FOLDER -type f -exec sed -i '' "s//$PACKAGE_NAME/g" {} \; - -# Greet the user -echo "Package created in $PACKAGE_FOLDER" +set -euo pipefail +exec python3 "$(dirname "$0")/scaffold.py" package "$@" diff --git a/scripts/new-service.sh b/scripts/new-service.sh index 0816814..9da692d 100644 --- a/scripts/new-service.sh +++ b/scripts/new-service.sh @@ -1,48 +1,3 @@ #!/usr/bin/env bash - -# Change folder to the root of the project -cd "$(dirname "$0")/.." - -# Get the service name from the first argument -SERVICE_NAME=$1 - -# If service name is empty, exit -if [ -z "$SERVICE_NAME" ]; then - echo "Service name is required" - exit 1 -fi - -# Validate the service name, must be alphanumeric, non empty, and no spaces. Dashes is allowed. -if [[ ! $SERVICE_NAME =~ ^[a-zA-Z0-9-]+$ ]]; then - echo "Invalid service name: $SERVICE_NAME" - echo "Service name must be alphanumeric, non empty, and no spaces. Dashes is allowed." - exit 1 -fi - -# If service already exists, exit -if [ -d "services/$SERVICE_NAME" ]; then - echo "Service already exists: $SERVICE_NAME" - exit 1 -fi - -# Create a copy of the service name, while replacing dashes with underscores -SERVICE_NAME_UNDERSCORES=$(echo $SERVICE_NAME | sed 's/-/_/g') - -# Set the service folder -SERVICE_FOLDER=services/$SERVICE_NAME_UNDERSCORES - -# Copy the service template to the services directory -cp -r templates/service $SERVICE_FOLDER - -# Rename the service template files to the service name -mv $SERVICE_FOLDER/src/service_name $SERVICE_FOLDER/src/$SERVICE_NAME_UNDERSCORES - -export LC_ALL=C -# Replace all instances of "" with the service name, in all files in the service directory -find $SERVICE_FOLDER -type f -exec sed -i '' "s//$SERVICE_NAME_UNDERSCORES/g" {} \; - -# Replace all instances of "" with the service name, in all files in the service directory -find $SERVICE_FOLDER -type f -exec sed -i '' "s//$SERVICE_NAME/g" {} \; - -# Greet the user -echo "Service created in $SERVICE_FOLDER" +set -euo pipefail +exec python3 "$(dirname "$0")/scaffold.py" service "$@" diff --git a/scripts/scaffold.py b/scripts/scaffold.py new file mode 100644 index 0000000..30ecc3b --- /dev/null +++ b/scripts/scaffold.py @@ -0,0 +1,71 @@ +"""Create workspace members without platform-specific shell substitutions.""" + +import argparse +import keyword +import re +import shutil +import sys +import tempfile +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def create(kind: str, name: str, root: Path = ROOT) -> Path: + if kind not in {"package", "service"}: + raise ValueError("Expected package or service.") + if not re.fullmatch(r"[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*", name): + raise ValueError( + "Use a lowercase name starting with a letter, with hyphens or underscores." + ) + module = name.replace("-", "_") + if keyword.iskeyword(module) or keyword.issoftkeyword(module): + raise ValueError("The name must not be a Python keyword.") + if module in sys.stdlib_module_names: + raise ValueError("The import name must not shadow a Python standard library module.") + canonical = name.replace("_", "-") + for folder in ("services", "packages"): + for existing in (root / folder).iterdir(): + names = [existing.name] + metadata = existing / "pyproject.toml" + if metadata.is_file(): + project = tomllib.loads(metadata.read_text(encoding="utf-8")).get("project", {}) + names.append(project.get("name", existing.name)) + if any(re.sub(r"[-_.]+", "-", value).lower() == canonical for value in names): + raise ValueError(f"A workspace member already exists: {existing.name}") + destination = root / f"{kind}s" / name + # Stage the complete result so a failed render cannot leave a partial project. + with tempfile.TemporaryDirectory(dir=root) as staging: + staged = Path(staging) / name + shutil.copytree( + root / "templates" / kind, + staged, + ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache", ".ruff_cache", ".venv"), + ) + (staged / "src" / f"{kind}_name").rename(staged / "src" / module) + for path in staged.rglob("*"): + if path.is_file(): + content = path.read_text(encoding="utf-8") + content = content.replace(f"<{kind.upper()}-NAME>", name) + content = content.replace(f"<{kind.upper()}_NAME>", module) + content = content.replace(f"{kind}_name", module) + path.write_text(content, encoding="utf-8", newline="\n") + staged.rename(destination) + return destination + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("kind", choices=("package", "service")) + parser.add_argument("name") + args = parser.parse_args() + try: + destination = create(args.kind, args.name) + except ValueError as error: + parser.error(str(error)) + print(f"Created {destination.relative_to(ROOT)}. Run uv lock, then uv sync --all-packages.") + + +if __name__ == "__main__": + main() diff --git a/scripts/smoke_test.py b/scripts/smoke_test.py new file mode 100644 index 0000000..ff8faff --- /dev/null +++ b/scripts/smoke_test.py @@ -0,0 +1,140 @@ +"""Exercise generated projects, installed wheels, and optionally their production image.""" + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +LIBRARY = "template-smoke-library" +SERVICE = "template-smoke-service" +GREETING = f"Hello from {SERVICE}!" + + +def run(arguments: list[str], root: Path) -> str: + # Nested uv commands must use the temporary workspace's environment. + environment = os.environ.copy() + environment.pop("VIRTUAL_ENV", None) + environment.pop("UV_PROJECT_ENVIRONMENT", None) + print(f"Running: {' '.join(arguments)}", flush=True) + result = subprocess.run( + arguments, + cwd=root, + env=environment, + check=False, + text=True, + stdout=subprocess.PIPE, + ) + print(result.stdout, end="", flush=True) + result.check_returncode() + return result.stdout + + +def smoke(root: Path, *, docker: bool = False) -> None: + for file in ( + "pyproject.toml", + "uv.lock", + ".python-version", + ".dockerignore", + ".gitignore", + "LICENSE", + "Makefile", + ): + shutil.copy2(ROOT / file, root / file) + for folder in ("scripts", "templates", "tests"): + shutil.copytree( + ROOT / folder, + root / folder, + ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache", ".ruff_cache", ".venv"), + ) + for folder in ("services", "packages"): + (root / folder).mkdir() + for kind, name in (("package", LIBRARY), ("service", SERVICE)): + run([sys.executable, "scripts/scaffold.py", kind, name], root) + library_source = root / "packages" / LIBRARY / "src/template_smoke_library/__init__.py" + greeting_code = f'def greeting() -> str:\n return "{GREETING}"\n' + library_source.write_text(greeting_code, encoding="utf-8") + service_source = root / "services" / SERVICE / "src/template_smoke_service/__main__.py" + service_source.write_text( + "from template_smoke_library import greeting\n\n\n" + "def main() -> None:\n print(greeting())\n\n\n" + 'if __name__ == "__main__":\n main()\n', + encoding="utf-8", + ) + run(["uv", "add", "--package", SERVICE, "--workspace", LIBRARY], root) + run(["uv", "sync", "--all-packages", "--locked"], root) + run(["uv", "run", "--all-packages", "--locked", "ruff", "check", "."], root) + run( + ["uv", "run", "--all-packages", "--locked", "ruff", "format", "--check", "--diff", "."], + root, + ) + run(["uv", "run", "--all-packages", "--locked", "ty", "check"], root) + run(["uv", "run", "--all-packages", "--locked", "python", "scripts/test_all.py"], root) + output = run(["uv", "run", "--locked", "--package", SERVICE, SERVICE], root) + if output.strip() != GREETING: + raise RuntimeError("The service did not return the expected greeting.") + run(["uv", "build", "--all-packages", "--out-dir", "dist"], root) + # Install built distributions in a fresh environment with no source path or dev dependencies. + runtime = root / "wheel-runtime" + run(["uv", "venv", str(runtime)], root) + python = runtime / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") + wheels = sorted(str(path) for path in (root / "dist").glob("*.whl")) + if len(wheels) != 2: + raise RuntimeError(f"Expected two generated wheels, found {len(wheels)}.") + run(["uv", "pip", "install", "--python", str(python), "--no-deps", *wheels], root) + probe = f""" +from importlib.metadata import distribution +from importlib.resources import files +from template_smoke_service.__main__ import main +for name in ("template_smoke_library", "template_smoke_service"): + assert files(name).joinpath("py.typed").is_file(), name +metadata = distribution("{SERVICE}") +assert any(req.startswith("{LIBRARY}") for req in metadata.requires or []) +assert all("file:" not in req for req in metadata.requires or []) +assert any("LICENSE" in str(file) for file in metadata.files or []) +main() +""" + output = run([str(python), "-I", "-c", probe], root) + if output.strip() != GREETING: + raise RuntimeError("The installed wheels did not return the expected greeting.") + if docker: + tag = "monorepo-template-smoke:local" + build = ["docker", "build", "-t", tag, "-f", f"services/{SERVICE}/Dockerfile"] + # The CA is mounted only for the build and is never copied into the image. + certificate = os.environ.get("CODEX_PROXY_CERT") + if certificate: + build.extend(["--secret", f"id=proxy_ca,src={certificate}"]) + run([*build, "."], root) + output = run(["docker", "run", "--rm", "--init", tag], root) + if output.strip() != GREETING: + raise RuntimeError("The Docker service did not return the expected greeting.") + run( + [ + "docker", + "run", + "--rm", + tag, + "python", + "-I", + "-c", + "import os; from importlib.util import find_spec; " + "assert os.getuid() == 10001; assert find_spec('pytest') is None", + ], + root, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--docker", action="store_true", help="Build and run the service image") + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix="monorepo-smoke-") as temporary: + smoke(Path(temporary), docker=args.docker) + print("Smoke checks passed.") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_all.py b/scripts/test_all.py new file mode 100644 index 0000000..4970449 --- /dev/null +++ b/scripts/test_all.py @@ -0,0 +1,24 @@ +"""Run each project's tests separately to avoid module and configuration collisions.""" + +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def main() -> None: + projects = [ROOT] + for folder in ("packages", "services"): + projects.extend(sorted(path.parent for path in (ROOT / folder).glob("*/pyproject.toml"))) + for project in projects: + print(f"Testing {project.relative_to(ROOT)}", flush=True) + subprocess.run( + [sys.executable, "-m", "pytest", "tests", *sys.argv[1:]], + cwd=project, + check=True, + ) + + +if __name__ == "__main__": + main() diff --git a/templates/package/LICENSE b/templates/package/LICENSE new file mode 100644 index 0000000..27976df --- /dev/null +++ b/templates/package/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 matanby + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/templates/package/Makefile b/templates/package/Makefile index b456100..202f3ca 100644 --- a/templates/package/Makefile +++ b/templates/package/Makefile @@ -1,15 +1,20 @@ -install: ## Install the package +.DEFAULT_GOAL := help +.PHONY: help typecheck install format lint test + +install: ## Install @bash scripts/install.sh $(args) -format: ## Format the code +format: ## Format @bash scripts/format.sh $(args) -lint: ## Lint the code +lint: ## Lint @bash scripts/lint.sh $(args) -test: ## Run the tests +test: ## Test @bash scripts/test.sh $(args) -.PHONY: help -help: - @awk 'BEGIN {FS = ":.*##"; printf "Usage: make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +typecheck: ## Check Python types + @bash scripts/typecheck.sh $(args) + +help: ## Show available commands + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/templates/package/README.md b/templates/package/README.md index e69de29..64be5a9 100644 --- a/templates/package/README.md +++ b/templates/package/README.md @@ -0,0 +1,9 @@ +# + +Requires uv and Python 3.12+. Run `uv lock` at the repository root after creation, +then `make install` here (or `uv sync --all-packages --locked` at the root). + +- `make test`: test this package. +- `make lint`: check code with Ruff. +- `make format`: format code with Ruff. +- `make typecheck`: check Python types with ty. diff --git a/templates/package/poetry.lock b/templates/package/poetry.lock deleted file mode 100644 index 65af547..0000000 --- a/templates/package/poetry.lock +++ /dev/null @@ -1,406 +0,0 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "23.3.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] - -[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.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] - -[[package]] -name = "filelock" -version = "3.11.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"}, - {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"}, -] - -[package.extras] -docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "identify" -version = "2.5.22" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, - {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nodeenv" -version = "1.7.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] - -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - -[[package]] -name = "platformdirs" -version = "3.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, - {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, -] - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.2.2" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"}, - {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pytest" -version = "7.2.2" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] - -[[package]] -name = "ruff" -version = "0.0.261" -description = "An extremely fast Python linter, written in Rust." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.0.261-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:6624a966c4a21110cee6780333e2216522a831364896f3d98f13120936eff40a"}, - {file = "ruff-0.0.261-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2dba68a9e558ab33e6dd5d280af798a2d9d3c80c913ad9c8b8e97d7b287f1cc9"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd0cee5a81b0785dc0feeb2640c1e31abe93f0d77c5233507ac59731a626f1"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581e64fa1518df495ca890a605ee65065101a86db56b6858f848bade69fc6489"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc970f6ece0b4950e419f0252895ee42e9e8e5689c6494d18f5dc2c6ebb7f798"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fa98e747e0fe185d65a40b0ea13f55c492f3b5f9a032a1097e82edaddb9e52e"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f268d52a71bf410aa45c232870c17049df322a7d20e871cfe622c9fc784aab7b"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1293acc64eba16a11109678dc4743df08c207ed2edbeaf38b3e10eb2597321b"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d95596e2f4cafead19a6d1ec0b86f8fda45ba66fe934de3956d71146a87959b3"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4bcec45abdf65c1328a269cf6cc193f7ff85b777fa2865c64cf2c96b80148a2c"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c5f397ec0af42a434ad4b6f86565027406c5d0d0ebeea0d5b3f90c4bf55bc82"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_i686.whl", hash = "sha256:39abd02342cec0c131b2ddcaace08b2eae9700cab3ca7dba64ae5fd4f4881bd0"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aaa4f52a6e513f8daa450dac4859e80390d947052f592f0d8e796baab24df2fc"}, - {file = "ruff-0.0.261-py3-none-win32.whl", hash = "sha256:daff64b4e86e42ce69e6367d63aab9562fc213cd4db0e146859df8abc283dba0"}, - {file = "ruff-0.0.261-py3-none-win_amd64.whl", hash = "sha256:0fbc689c23609edda36169c8708bb91bab111d8f44cb4a88330541757770ab30"}, - {file = "ruff-0.0.261-py3-none-win_arm64.whl", hash = "sha256:d2eddc60ae75fc87f8bb8fd6e8d5339cf884cd6de81e82a50287424309c187ba"}, - {file = "ruff-0.0.261.tar.gz", hash = "sha256:c1c715b0d1e18f9c509d7c411ca61da3543a4aa459325b1b1e52b8301d65c6d2"}, -] - -[[package]] -name = "setuptools" -version = "67.6.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "virtualenv" -version = "20.21.0" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, - {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, -] - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "bf431a151cb77fc848508fae750f5ad8ebc617f0ff37df5601c014f26c8afb46" diff --git a/templates/package/pyproject.toml b/templates/package/pyproject.toml index 38a0739..6958676 100644 --- a/templates/package/pyproject.toml +++ b/templates/package/pyproject.toml @@ -1,19 +1,26 @@ -[tool.poetry] +[project] name = "" version = "0.1.0" +license = "MIT" +license-files = ["LICENSE"] description = "" -authors = [] -packages = [{include = "", from = "src"}] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] -[tool.poetry.dependencies] -python = "^3.11" +[build-system] +requires = ["hatchling>=1.27,<2"] +build-backend = "hatchling.build" -[tool.poetry.group.dev.dependencies] -black = "*" -pre-commit = "*" -pytest = "*" -ruff = "*" +[tool.hatch.build.targets.wheel] +packages = ["src/"] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --import-mode=importlib" + +[tool.ty.environment] +python-version = "3.12" diff --git a/templates/package/scripts/format.sh b/templates/package/scripts/format.sh index 4c94339..35e9782 100644 --- a/templates/package/scripts/format.sh +++ b/templates/package/scripts/format.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -PACKAGE_ROOT=$(realpath $(dirname $0)/../) -SRC_FOLDER=$PACKAGE_ROOT/src -TESTS_FOLDER=$PACKAGE_ROOT/tests -ARGS=$@ - -poetry run black $SRC_FOLDER $TESTS_FOLDER $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked ruff format src tests "$@" diff --git a/templates/package/scripts/install.sh b/templates/package/scripts/install.sh index c26f39a..e019c3c 100644 --- a/templates/package/scripts/install.sh +++ b/templates/package/scripts/install.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash -ARGS=$@ -poetry install $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv sync --all-packages --locked "$@" diff --git a/templates/package/scripts/lint.sh b/templates/package/scripts/lint.sh index 3f013b7..529a153 100644 --- a/templates/package/scripts/lint.sh +++ b/templates/package/scripts/lint.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -PACKAGE_ROOT=$(realpath $(dirname $0)/../) -SRC_FOLDER=$PACKAGE_ROOT/src -TESTS_FOLDER=$PACKAGE_ROOT/tests -ARGS=$@ - -poetry run ruff $SRC_FOLDER $TESTS_FOLDER $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked ruff check src tests "$@" diff --git a/templates/package/scripts/test.sh b/templates/package/scripts/test.sh index 197f497..ec19f98 100644 --- a/templates/package/scripts/test.sh +++ b/templates/package/scripts/test.sh @@ -1,7 +1,4 @@ #!/usr/bin/env bash - -PACKAGE_ROOT=$(realpath $(dirname $0)/../) -TESTS_FOLDER=$PACKAGE_ROOT/tests -ARGS=$@ - -poetry run pytest -rav $TESTS_FOLDER $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked pytest tests "$@" diff --git a/templates/package/scripts/typecheck.sh b/templates/package/scripts/typecheck.sh new file mode 100644 index 0000000..b3f86f4 --- /dev/null +++ b/templates/package/scripts/typecheck.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked ty check src tests "$@" diff --git a/templates/package/src/package_name/__init__.py b/templates/package/src/package_name/__init__.py index e69de29..2a2b7bb 100644 --- a/templates/package/src/package_name/__init__.py +++ b/templates/package/src/package_name/__init__.py @@ -0,0 +1 @@ +""".""" diff --git a/templates/package/src/package_name/py.typed b/templates/package/src/package_name/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/templates/package/tests/test_dummy.py b/templates/package/tests/test_dummy.py deleted file mode 100644 index 3fb8a97..0000000 --- a/templates/package/tests/test_dummy.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_dummy() -> None: - assert True diff --git a/templates/package/tests/test_package.py b/templates/package/tests/test_package.py new file mode 100644 index 0000000..2429a45 --- /dev/null +++ b/templates/package/tests/test_package.py @@ -0,0 +1,8 @@ +import unittest + +import package_name + + +class PackageTests(unittest.TestCase): + def test_package_is_importable(self) -> None: + self.assertEqual(package_name.__name__, "package_name") diff --git a/templates/service/Dockerfile b/templates/service/Dockerfile index 931fb8a..e5eae7b 100644 --- a/templates/service/Dockerfile +++ b/templates/service/Dockerfile @@ -1,32 +1,19 @@ -FROM python:3.11 - -# install Poetry -RUN pip install --no-cache-dir poetry - -# set the home directory -ENV APP_HOME /opt -ENV SERVICE_HOME $APP_HOME/services/ - -# copy `facefx-worker` Poetry files -WORKDIR $SERVICE_HOME -COPY pyproject.toml . -COPY poetry.lock . - -# create a dummy package to satisfy Poetry -RUN mkdir -p src/ && touch src//__init__.py - -# install requirements -RUN poetry install --only main -n && rm -rf ~/.cache/pypoetry/cache - -# copy source code -COPY src src - -# store the git branch and commit hash as env variables -ARG GIT_BRANCH -ARG GIT_COMMIT_HASH -ENV GIT_BRANCH=${GIT_BRANCH} -ENV GIT_COMMIT_HASH=${GIT_COMMIT_HASH} - -# set starting command -WORKDIR $SERVICE_HOME -CMD poetry run python src/run.py +# syntax=docker/dockerfile:1 +FROM python:3.14-slim-trixie AS builder +COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /usr/local/bin/uv +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never +WORKDIR /app +# Build with the repository root as context so workspace dependencies are available. +COPY . . +# Optional CA secret supports builds behind an HTTPS proxy. +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=secret,id=proxy_ca \ + if [ -f /run/secrets/proxy_ca ]; then export SSL_CERT_FILE=/run/secrets/proxy_ca; fi; \ + uv sync --locked --no-dev --no-editable --package "" + +FROM python:3.14-slim-trixie AS runtime +ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 PATH="/app/.venv/bin:$PATH" +WORKDIR /app +COPY --from=builder /app/.venv /app/.venv +USER 10001:10001 +CMD [""] diff --git a/templates/service/LICENSE b/templates/service/LICENSE new file mode 100644 index 0000000..27976df --- /dev/null +++ b/templates/service/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 matanby + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/templates/service/Makefile b/templates/service/Makefile index 584c7c1..29860de 100644 --- a/templates/service/Makefile +++ b/templates/service/Makefile @@ -1,24 +1,29 @@ -install: ## Install dependencies - @bash scripts/install.sh $(args) +.DEFAULT_GOAL := help +.PHONY: help typecheck install format lint test run docker-build docker-run -run: ## Run the service - @bash scripts/run.sh $(args) +install: ## Install + @bash scripts/install.sh $(args) -format: ## Format the code +format: ## Format @bash scripts/format.sh $(args) -lint: ## Lint the code +lint: ## Lint @bash scripts/lint.sh $(args) -test: ## Run the tests +test: ## Test @bash scripts/test.sh $(args) -docker-build: ## Build the docker image +run: ## Run + @bash scripts/run.sh $(args) + +docker-build: ## Docker build @bash scripts/docker-build.sh $(args) -docker-run: ## Run the docker image +docker-run: ## Docker run @bash scripts/docker-run.sh $(args) -.PHONY: help -help: - @awk 'BEGIN {FS = ":.*##"; printf "Usage: make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +typecheck: ## Check Python types + @bash scripts/typecheck.sh $(args) + +help: ## Show available commands + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/templates/service/README.md b/templates/service/README.md index e69de29..cc9fcaa 100644 --- a/templates/service/README.md +++ b/templates/service/README.md @@ -0,0 +1,12 @@ +# + +Requires uv and Python 3.12+. Run `uv lock` at the repository root after creation, +then `make install` here (or `uv sync --all-packages --locked` at the root). + +- `make test`: test this service. +- `make lint`: check code with Ruff. +- `make format`: format code with Ruff. +- `make run`: run the installed service entry point. +- `make docker-build`: build from the repository root context, including workspace packages. +- `make docker-run`: run the image. +- `make typecheck`: check Python types with ty. diff --git a/templates/service/poetry.lock b/templates/service/poetry.lock deleted file mode 100644 index 65af547..0000000 --- a/templates/service/poetry.lock +++ /dev/null @@ -1,406 +0,0 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "23.3.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] - -[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.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] - -[[package]] -name = "filelock" -version = "3.11.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"}, - {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"}, -] - -[package.extras] -docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "identify" -version = "2.5.22" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, - {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nodeenv" -version = "1.7.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] - -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - -[[package]] -name = "platformdirs" -version = "3.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, - {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, -] - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.2.2" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"}, - {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pytest" -version = "7.2.2" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] - -[[package]] -name = "ruff" -version = "0.0.261" -description = "An extremely fast Python linter, written in Rust." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.0.261-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:6624a966c4a21110cee6780333e2216522a831364896f3d98f13120936eff40a"}, - {file = "ruff-0.0.261-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2dba68a9e558ab33e6dd5d280af798a2d9d3c80c913ad9c8b8e97d7b287f1cc9"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd0cee5a81b0785dc0feeb2640c1e31abe93f0d77c5233507ac59731a626f1"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581e64fa1518df495ca890a605ee65065101a86db56b6858f848bade69fc6489"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc970f6ece0b4950e419f0252895ee42e9e8e5689c6494d18f5dc2c6ebb7f798"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fa98e747e0fe185d65a40b0ea13f55c492f3b5f9a032a1097e82edaddb9e52e"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f268d52a71bf410aa45c232870c17049df322a7d20e871cfe622c9fc784aab7b"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1293acc64eba16a11109678dc4743df08c207ed2edbeaf38b3e10eb2597321b"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d95596e2f4cafead19a6d1ec0b86f8fda45ba66fe934de3956d71146a87959b3"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4bcec45abdf65c1328a269cf6cc193f7ff85b777fa2865c64cf2c96b80148a2c"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c5f397ec0af42a434ad4b6f86565027406c5d0d0ebeea0d5b3f90c4bf55bc82"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_i686.whl", hash = "sha256:39abd02342cec0c131b2ddcaace08b2eae9700cab3ca7dba64ae5fd4f4881bd0"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aaa4f52a6e513f8daa450dac4859e80390d947052f592f0d8e796baab24df2fc"}, - {file = "ruff-0.0.261-py3-none-win32.whl", hash = "sha256:daff64b4e86e42ce69e6367d63aab9562fc213cd4db0e146859df8abc283dba0"}, - {file = "ruff-0.0.261-py3-none-win_amd64.whl", hash = "sha256:0fbc689c23609edda36169c8708bb91bab111d8f44cb4a88330541757770ab30"}, - {file = "ruff-0.0.261-py3-none-win_arm64.whl", hash = "sha256:d2eddc60ae75fc87f8bb8fd6e8d5339cf884cd6de81e82a50287424309c187ba"}, - {file = "ruff-0.0.261.tar.gz", hash = "sha256:c1c715b0d1e18f9c509d7c411ca61da3543a4aa459325b1b1e52b8301d65c6d2"}, -] - -[[package]] -name = "setuptools" -version = "67.6.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "virtualenv" -version = "20.21.0" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, - {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, -] - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "bf431a151cb77fc848508fae750f5ad8ebc617f0ff37df5601c014f26c8afb46" diff --git a/templates/service/pyproject.toml b/templates/service/pyproject.toml index 602fb39..d5fcd8b 100644 --- a/templates/service/pyproject.toml +++ b/templates/service/pyproject.toml @@ -1,19 +1,29 @@ -[tool.poetry] +[project] name = "" version = "0.1.0" +license = "MIT" +license-files = ["LICENSE"] description = "" -authors = [] -packages = [{include = "", from = "src"}] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] -[tool.poetry.dependencies] -python = "^3.11" - -[tool.poetry.group.dev.dependencies] -black = "*" -pre-commit = "*" -pytest = "*" -ruff = "*" +[project.scripts] +"" = ".__main__:main" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling>=1.27,<2"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/"] + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --import-mode=importlib" + +[tool.ty.environment] +python-version = "3.12" diff --git a/templates/service/scripts/docker-build.sh b/templates/service/scripts/docker-build.sh index 4dad694..4ab62bd 100644 --- a/templates/service/scripts/docker-build.sh +++ b/templates/service/scripts/docker-build.sh @@ -1,7 +1,4 @@ #!/usr/bin/env bash - -SERVICE_ROOT=$(realpath $(dirname $0)/../) -DOCKERFILE_PATH=${SERVICE_ROOT}/Dockerfile -ARGS=("$@") - -docker build -t :latest -f ${DOCKERFILE_PATH} ${SERVICE_ROOT} ${ARGS[@]} +set -euo pipefail +cd "$(dirname "$0")/.." +exec docker build -t ":latest" -f Dockerfile ../.. "$@" diff --git a/templates/service/scripts/docker-run.sh b/templates/service/scripts/docker-run.sh index 5f6977e..8fe9b4d 100644 --- a/templates/service/scripts/docker-run.sh +++ b/templates/service/scripts/docker-run.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -ARGS="$@" - -docker run -it --rm \ - --name \ - :latest \ - $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec docker run --rm --init ":latest" "$@" diff --git a/templates/service/scripts/format.sh b/templates/service/scripts/format.sh index 2609de0..35e9782 100644 --- a/templates/service/scripts/format.sh +++ b/templates/service/scripts/format.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -SERVICE_ROOT=$(realpath $(dirname $0)/../) -SRC_FOLDER=$SERVICE_ROOT/src -TESTS_FOLDER=$SERVICE_ROOT/tests -ARGS=$@ - -poetry run black $SRC_FOLDER $TESTS_FOLDER $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked ruff format src tests "$@" diff --git a/templates/service/scripts/install.sh b/templates/service/scripts/install.sh index c60a3c4..e019c3c 100644 --- a/templates/service/scripts/install.sh +++ b/templates/service/scripts/install.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash - -ARGS="$@" -poetry install $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv sync --all-packages --locked "$@" diff --git a/templates/service/scripts/lint.sh b/templates/service/scripts/lint.sh index 993b476..529a153 100644 --- a/templates/service/scripts/lint.sh +++ b/templates/service/scripts/lint.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -SERVICE_ROOT=$(realpath $(dirname $0)/../) -SRC_FOLDER=$SERVICE_ROOT/src -TESTS_FOLDER=$SERVICE_ROOT/tests -ARGS=$@ - -poetry run ruff $SRC_FOLDER $TESTS_FOLDER $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked ruff check src tests "$@" diff --git a/templates/service/scripts/run.sh b/templates/service/scripts/run.sh index 360cd7e..cbb6055 100644 --- a/templates/service/scripts/run.sh +++ b/templates/service/scripts/run.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash - -ARGS="$@" -poetry run python src/run.py $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked --package "" "" "$@" diff --git a/templates/service/scripts/test.sh b/templates/service/scripts/test.sh index e1b9d53..ec19f98 100644 --- a/templates/service/scripts/test.sh +++ b/templates/service/scripts/test.sh @@ -1,7 +1,4 @@ #!/usr/bin/env bash - -SERVICE_ROOT=$(realpath $(dirname $0)/../) -TESTS_FOLDER=$SERVICE_ROOT/tests -ARGS=$@ - -poetry run pytest -rav $TESTS_FOLDER $ARGS +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked pytest tests "$@" diff --git a/templates/service/scripts/typecheck.sh b/templates/service/scripts/typecheck.sh new file mode 100644 index 0000000..b3f86f4 --- /dev/null +++ b/templates/service/scripts/typecheck.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +exec uv run --locked ty check src tests "$@" diff --git a/templates/service/src/service_name/__init__.py b/templates/service/src/service_name/__init__.py index e69de29..4e50ddd 100644 --- a/templates/service/src/service_name/__init__.py +++ b/templates/service/src/service_name/__init__.py @@ -0,0 +1 @@ +""".""" diff --git a/templates/service/src/run.py b/templates/service/src/service_name/__main__.py similarity index 52% rename from templates/service/src/run.py rename to templates/service/src/service_name/__main__.py index 806de08..fc38df6 100644 --- a/templates/service/src/run.py +++ b/templates/service/src/service_name/__main__.py @@ -1,5 +1,5 @@ def main() -> None: - print("Hello from !") # noqa: T201 + print("Hello from !") if __name__ == "__main__": diff --git a/templates/service/src/service_name/py.typed b/templates/service/src/service_name/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/templates/service/tests/test_dummy.py b/templates/service/tests/test_dummy.py deleted file mode 100644 index 3fb8a97..0000000 --- a/templates/service/tests/test_dummy.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_dummy() -> None: - assert True diff --git a/templates/service/tests/test_service.py b/templates/service/tests/test_service.py new file mode 100644 index 0000000..b386f35 --- /dev/null +++ b/templates/service/tests/test_service.py @@ -0,0 +1,13 @@ +import io +import unittest +from contextlib import redirect_stdout + +from service_name.__main__ import main + + +class ServiceTests(unittest.TestCase): + def test_main(self) -> None: + output = io.StringIO() + with redirect_stdout(output): + main() + self.assertEqual(output.getvalue(), "Hello from !\n") diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py new file mode 100644 index 0000000..5729dfb --- /dev/null +++ b/tests/test_scaffold.py @@ -0,0 +1,144 @@ +import importlib.util +import os +import shutil +import subprocess +import sys +import tempfile +import tomllib +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("scaffold", ROOT / "scripts/scaffold.py") +assert SPEC is not None and SPEC.loader is not None +scaffold = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(scaffold) + + +class ScaffoldTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory(prefix="template test ") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + shutil.copytree(ROOT / "templates", self.root / "templates") + for folder in ("services", "packages"): + (self.root / folder).mkdir() + + def test_generated_projects(self) -> None: + for kind in ("package", "service"): + with self.subTest(kind=kind): + name = f"hello-{kind}" + module = name.replace("-", "_") + project = scaffold.create(kind, name, self.root) + metadata = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8")) + self.assertEqual(metadata["project"]["name"], name) + self.assertTrue((project / "src" / module / "__init__.py").is_file()) + self.assertTrue((project / "src" / module / "py.typed").is_file()) + self.assertEqual( + (project / "LICENSE").read_text(encoding="utf-8"), + (ROOT / "LICENSE").read_text(encoding="utf-8"), + ) + for path in project.rglob("*"): + if path.is_file(): + content = path.read_text(encoding="utf-8") + self.assertNotIn(f"<{kind.upper()}", content) + self.assertNotIn(f"{kind}_name", content) + self.assertNotIn(b"\r\n", path.read_bytes()) + environment = os.environ.copy() + environment["PYTHONPATH"] = str(project / "src") + subprocess.run( + [sys.executable, "-m", "unittest", "discover", "-s", "tests"], + cwd=project, + env=environment, + capture_output=True, + check=True, + ) + if kind == "service": + result = subprocess.run( + [sys.executable, str(project / "src" / module / "__main__.py")], + check=True, + text=True, + capture_output=True, + ) + self.assertEqual(result.stdout, f"Hello from {name}!\n") + + def test_invalid_names_leave_repository_untouched(self) -> None: + for name in ("", "../escape", "123", "Upper", "has space", "a--b", "class", "logging"): + with self.subTest(name=name), self.assertRaises(ValueError): + scaffold.create("package", name, self.root) + self.assertEqual(list((self.root / "packages").iterdir()), []) + + def test_invalid_kind_is_rejected(self) -> None: + with self.assertRaises(ValueError): + scaffold.create("../escape", "valid-name", self.root) + + def test_normalized_collision_does_not_overwrite(self) -> None: + original = scaffold.create("package", "shared-utils", self.root) + marker = original / "keep.txt" + marker.write_text("keep me", encoding="utf-8") + for kind in ("package", "service"): + for name in ("shared-utils", "shared_utils"): + with self.assertRaisesRegex(ValueError, "already exists"): + scaffold.create(kind, name, self.root) + self.assertEqual(marker.read_text(encoding="utf-8"), "keep me") + + def test_distribution_name_collision_is_rejected(self) -> None: + existing = self.root / "services" / "different-directory" + existing.mkdir() + (existing / "pyproject.toml").write_text( + '[project]\nname = "Shared.Utils"\n', encoding="utf-8" + ) + with self.assertRaisesRegex(ValueError, "already exists"): + scaffold.create("package", "shared-utils", self.root) + + def test_template_failure_leaves_no_partial_project(self) -> None: + shutil.rmtree(self.root / "templates/service/src/service_name") + with self.assertRaises(FileNotFoundError): + scaffold.create("service", "broken", self.root) + self.assertEqual(list((self.root / "services").iterdir()), []) + self.assertEqual( + sorted(path.name for path in self.root.iterdir()), ["packages", "services", "templates"] + ) + + def test_underscore_name_and_cache_exclusion(self) -> None: + cache = self.root / "templates/package/src/package_name/__pycache__" + cache.mkdir(exist_ok=True) + (cache / "stale.pyc").write_bytes(b"cached bytecode") + project = scaffold.create("package", "shared_utils", self.root) + self.assertTrue((project / "src/shared_utils/__init__.py").is_file()) + self.assertEqual(list(project.rglob("*.pyc")), []) + + @unittest.skipUnless(shutil.which("make"), "GNU Make is optional") + def test_make_passes_invalid_name_as_one_argument(self) -> None: + shutil.copy2(ROOT / "Makefile", self.root / "Makefile") + shutil.copytree( + ROOT / "scripts", self.root / "scripts", ignore=shutil.ignore_patterns("__pycache__") + ) + result = subprocess.run( + ["make", "new-package", 'name=bad"; touch unexpected; #'], + cwd=self.root, + capture_output=True, + text=True, + check=False, + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse((self.root / "unexpected").exists()) + self.assertEqual(list((self.root / "packages").iterdir()), []) + + def test_cli_works_outside_repository(self) -> None: + shutil.copytree( + ROOT / "scripts", self.root / "scripts", ignore=shutil.ignore_patterns("__pycache__") + ) + result = subprocess.run( + [sys.executable, str(self.root / "scripts/scaffold.py"), "package", "cli-project"], + cwd=self.root.parent, + capture_output=True, + text=True, + check=True, + ) + self.assertIn("packages/cli-project", result.stdout.replace("\\", "/")) + self.assertTrue((self.root / "packages/cli-project/pyproject.toml").is_file()) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..4c0089a --- /dev/null +++ b/uv.lock @@ -0,0 +1,290 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" }, +] + +[[package]] +name = "filelock" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/08/d8f81287742010e38daf3632380c8d80ede7b4df2716f8ca3d0a2b087d44/filelock-4.2.2.tar.gz", hash = "sha256:b29ff2ae9140bb17b3bc77c648dc4f010d9ac0955eccabff4f2b01dde859f787", size = 651339, upload-time = "2026-10-18T07:26:44.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/53/35698fff350cb99c49ee01ae5cb1eb4408567792a286314ea984b53b5002/filelock-4.2.2-py3-none-any.whl", hash = "sha256:1851d2f14ca2901e98069d68333b33ad7409f56904e17b48cc0c32a0db8ed73a", size = 162415, upload-time = "2026-10-18T07:26:43.115Z" }, +] + +[[package]] +name = "identify" +version = "2.6.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/89/dfd269de936e76319cf27787d9eaaca513491f635d49b0fbe7cdb6d3bf6f/platformdirs-4.13.4.tar.gz", hash = "sha256:dd96496961684a7f42041923f842ec7494e09f5e87aded99117e1109cd40570e", size = 62364, upload-time = "2026-10-17T19:31:53.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/51/5614d0537dd830ca64d95a2f254bd66bdf3ac61d41cffcccaec62121fcfd/platformdirs-4.13.4-py3-none-any.whl", hash = "sha256:502ce08e9d36cf90579ee44e1e764838842fa658e4975ef47344fd053011d2c8", size = 32782, upload-time = "2026-10-17T19:31:52.293Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/54/fad125f5c48f7821644ee6f223ebfcedc4e23597c1a16eeb13d33aa4a91a/python_discovery-1.6.3.tar.gz", hash = "sha256:a62b301d96cf5489cb96ab023b32e068421867fdd5df2e42eece877869047fa2", size = 88998, upload-time = "2026-10-16T03:54:29.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/ec/643826e4d69b7bdce4213f221e8f5ed06d0a9dd69fa9b33342578d2cc8c8/python_discovery-1.6.3-py3-none-any.whl", hash = "sha256:cb7654125e3dcb594269a6feb56ac693f1f13b7c654493a93eba72dc22d18034", size = 39386, upload-time = "2026-10-16T03:54:28.237Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, +] + +[[package]] +name = "ty" +version = "0.0.89" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/dfb9bdbc168c50a568809466596751afa0c7da60da03b8c12906d1decde4/ty-0.0.89.tar.gz", hash = "sha256:bc1027b1ea95cca25c2a3a0b41433c809cf4b86a384d2b2434eba4bcd34548d4", size = 7978241, upload-time = "2026-10-16T10:35:05.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/84/e199d9a3dc3c94c804ddd9e94b63dbe543ae7f39329574963b6a6a95872f/ty-0.0.89-py3-none-linux_armv6l.whl", hash = "sha256:739b087e8873e177ffda99ff8a3600a4a7704d06523a8e88c4493021eee40b8a", size = 14254879, upload-time = "2026-10-16T10:34:24.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/5afe09bf5c83c4e3a9e5913386a460e240a7029f2ae5fc96df5b43563a91/ty-0.0.89-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:47ebc98480c8a301955e18a8bdd67ef07090071ad47349b3254f325d3d5ab4e7", size = 13790417, upload-time = "2026-10-16T10:34:27.672Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f9/88648f0ba9400dfd642c1e8dc0aeecb87945b121f3ff51f3902883c016d6/ty-0.0.89-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0631291af5c850a15007c0ffb906d813966466e4ec12173c44977c0c5a2d51d", size = 12856900, upload-time = "2026-10-16T10:34:29.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/baae2b407101c4cb02f19216123900c147af8fd87cdb1ed7eba139472734/ty-0.0.89-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d768300ae16274ce656006e9199869ad68684b74400d963e1c82deddf3a22ad", size = 13653330, upload-time = "2026-10-16T10:34:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/293516c2e10a77134cfc40d22c3fc08edd8c5eadae9c75531ec8a9263348/ty-0.0.89-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dd4c57ba58bab8f6c816b2c254db52b58834b5dbf8268287382f7a2f9735c8f", size = 13986215, upload-time = "2026-10-16T10:34:34.506Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c3/da911c8fc22d2664692432570619576b25c7cd0a586401cab08987763094/ty-0.0.89-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1af774c24117d366b69142d4f35edee518617f8b926d13560d0334cbe5aa3f85", size = 14905008, upload-time = "2026-10-16T10:34:36.632Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/aed8d920b9bbf7b582e9fc0305acb2f2c748ede788e7dba9d6952e939c78/ty-0.0.89-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8147811b9b3bc2dc9338cd01c7893ac931fc3fae7aace169047ecbd627e6628", size = 15278217, upload-time = "2026-10-16T10:34:39.144Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/c6b4320f08fca44ffe279e5ddeb3eae12869c6341f596931accdefea1182/ty-0.0.89-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d5f178c91d50fb62461b5e579028b7d2e92872e1d8f02e96ace26464c2277a6", size = 15206289, upload-time = "2026-10-16T10:34:41.626Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/da22c714fd4f4dff642d60fb0d8bf8da1985d126f21cc89237f8bf707c92/ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a410f10b26cc3861757b626727db9d979deba7d2a3ab59f964fac8f0514fdc", size = 14510395, upload-time = "2026-10-16T10:34:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/f0/32/5f9ce2377a77ad9ff36d8996edf86b1f069c1e8716e37c7d6d668402b110/ty-0.0.89-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a70ab1a402746d7837407dc169e461fdf90945bc1eef0d3c0f1be543490060bf", size = 14767366, upload-time = "2026-10-16T10:34:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/2b28c71d6ebd97b5bb1378f1806598c5dd90278f7b99a431dd3879dddcfc/ty-0.0.89-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b55c28a49bd2676eac9b3540a1dcc4cc053e4671c65dcec9fdad3dfe55fb148c", size = 13670284, upload-time = "2026-10-16T10:34:48.225Z" }, + { url = "https://files.pythonhosted.org/packages/34/cc/68b8ebf98c9bc582fddc3d2b93f6e0f53719d1ddee1c5037dfa2e1cb8467/ty-0.0.89-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c9f22edeb8176f8ab189337e8cbd8f92b66744534a2886df9e252b52ade991c", size = 13997891, upload-time = "2026-10-16T10:34:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4d/a1d61e06092fd5726f48a8711bd32eac6af132308eb222a0027caba4f42d/ty-0.0.89-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c186185cad57a1469b851096b51345de7b625e353c1f4acd338402daed22fa7", size = 14326247, upload-time = "2026-10-16T10:34:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/466d3762e330504a2c114dbbd0f0383229f9652dccd56277dde008bfe0da/ty-0.0.89-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5e38302ac218d2404758241093b5542ed0345d4f569096589f1810034244e998", size = 14661527, upload-time = "2026-10-16T10:34:55.555Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d5/dfd7ed5e4af472b20a11ef651da4bca55f9dc8be485cd152337f94caddde/ty-0.0.89-py3-none-win32.whl", hash = "sha256:0146637ed2321717280bfe4e4b20a18813b741da280e8039d7df2bf22b0251f1", size = 13224078, upload-time = "2026-10-16T10:34:57.735Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/1ee663c422e3a2d910c5cc27e7d84bd7709a3df0aca28a713843c6c3fa5d/ty-0.0.89-py3-none-win_amd64.whl", hash = "sha256:9fc56322a1aff29a19088d2025f3c42a6d25a7035668048bb908c9f8b66f480d", size = 13727433, upload-time = "2026-10-16T10:35:00.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/af8f7250920823ca6d582bcf0abf803fb92c8b99415ba7f40f2304102c6f/ty-0.0.89-py3-none-win_arm64.whl", hash = "sha256:1d4967c8f7bfd67c9708115b41d7049197c5d20c76fb2e136fe78b0437ada05b", size = 13287101, upload-time = "2026-10-16T10:35:02.918Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.14.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/be/4d1d59d309db76b5808fcfd78bd5996f4e7241366691c38fc331a3136398/virtualenv-21.14.11.tar.gz", hash = "sha256:593052d8477ea8c55a842387ce111bd08c72618f0c1133c302cafe335e570a57", size = 5503091, upload-time = "2026-10-18T07:06:34.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/6b/d85da8982b41190eafcb9df91dff9d83fc6989e98af4b5be4c8f85510c95/virtualenv-21.14.11-py3-none-any.whl", hash = "sha256:b319f8590a84e5de5f8f2c803ecd2237e9bc0ffa7c1e875e3169c107cecb7454", size = 5495268, upload-time = "2026-10-18T07:06:32.719Z" }, +] + +[[package]] +name = "python-monorepo-template" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4,<5" }, + { name = "pytest", specifier = ">=8,<10" }, + { name = "ruff", specifier = ">=0.15,<1" }, + { name = "ty", specifier = ">=0.0.40,<0.1" }, +]