mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-01 17:00:48 +01:00
Initial commit
This commit is contained in:
207
.gitignore
vendored
Normal file
207
.gitignore
vendored
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
|
#pdm.lock
|
||||||
|
#pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
#pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# 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
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||||
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||||
|
# refer to https://docs.cursor.com/context/ignore-files
|
||||||
|
.cursorignore
|
||||||
|
.cursorindexingignore
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 d3vyce
|
||||||
|
|
||||||
|
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.
|
||||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# FastAPI Toolsets
|
||||||
|
|
||||||
|
FastAPI Toolsets provides production-ready utilities for FastAPI applications built with async SQLAlchemy and PostgreSQL. It includes generic CRUD operations, a fixture system with dependency resolution, a Django-like CLI, standardized API responses, and structured exception handling with automatic OpenAPI documentation.
|
||||||
|
|
||||||
|
[](https://github.com/astral-sh/ty)
|
||||||
|
[](https://github.com/astral-sh/uv)
|
||||||
|
[](https://github.com/astral-sh/ruff)
|
||||||
|
[](https://www.python.org/downloads/)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Documentation**: [https://fastapi-toolsets.d3vyce.fr](https://fastapi-toolsets.d3vyce.fr)
|
||||||
|
|
||||||
|
**Source Code**: [https://github.com/d3vyce/fastapi-toolsets](https://github.com/d3vyce/fastapi-toolsets)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv add fastapi-toolsets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **CRUD**: Generic async CRUD operations with `CrudFactory`
|
||||||
|
- **Fixtures**: Fixture system with dependency management, context support and pytest integration
|
||||||
|
- **CLI**: Django-like command-line interface for fixtures and custom commands
|
||||||
|
- **Standardized API Responses**: Consistent response format across your API
|
||||||
|
- **Exception Handling**: Structured error responses with automatic OpenAPI documentation
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit issues and pull requests.
|
||||||
81
pyproject.toml
Normal file
81
pyproject.toml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
[project]
|
||||||
|
name = "fastapi-toolsets"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
authors = [
|
||||||
|
{ name = "d3vyce", email = "contact@d3vyce.fr" }
|
||||||
|
]
|
||||||
|
keywords = ["fastapi", "sqlalchemy", "postgresql"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Framework :: AsyncIO",
|
||||||
|
"Framework :: FastAPI",
|
||||||
|
"Framework :: Pydantic",
|
||||||
|
"Framework :: SQLAlchemy",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: Information Technology",
|
||||||
|
"Intended Audience :: System Administrators",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Topic :: Database",
|
||||||
|
"Typing :: Typed",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.100.0",
|
||||||
|
"sqlalchemy[asyncio]>=2.0",
|
||||||
|
"asyncpg>=0.29.0",
|
||||||
|
"pydantic>=2.0",
|
||||||
|
"typer>=0.9.0",
|
||||||
|
"httpx>=0.25.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/d3vyce/fastapi-toolsets"
|
||||||
|
Documentation = "https://fastapi-toolsets.d3vyce.fr/"
|
||||||
|
Repository = "https://github.com/d3vyce/fastapi-toolsets"
|
||||||
|
Issues = "https://github.com/d3vyce/fastapi-toolsets/issues"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-anyio>=0.0.0",
|
||||||
|
"coverage>=7.0.0",
|
||||||
|
"pytest-cov>=4.0.0",
|
||||||
|
]
|
||||||
|
dev = [
|
||||||
|
"fastapi-toolsets[test]",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
"ty>=0.0.1a0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
fastapi-toolsets = "fastapi_toolsets.cli:app"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.9.26,<0.10.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
filterwarnings = [
|
||||||
|
"ignore::DeprecationWarning",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["src/fastapi_toolsets"]
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
]
|
||||||
24
src/fastapi_toolsets/__init__.py
Normal file
24
src/fastapi_toolsets/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""FastAPI utilities package.
|
||||||
|
|
||||||
|
Provides CRUD operations, fixtures, CLI, and standardized API responses
|
||||||
|
for FastAPI with async SQLAlchemy and PostgreSQL.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
from fastapi import FastAPI, Depends
|
||||||
|
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||||
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
|
from fastapi_toolsets.db import create_db_dependency
|
||||||
|
from fastapi_toolsets.schemas import Response
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
|
||||||
|
UserCrud = CrudFactory(User)
|
||||||
|
|
||||||
|
@app.get("/users/{user_id}", response_model=Response[dict])
|
||||||
|
async def get_user(user_id: int, session = Depends(get_db)):
|
||||||
|
user = await UserCrud.get(session, [User.id == user_id])
|
||||||
|
return Response(data={"user": user.username}, message="Success")
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
5
src/fastapi_toolsets/cli/__init__.py
Normal file
5
src/fastapi_toolsets/cli/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""CLI for FastAPI projects."""
|
||||||
|
|
||||||
|
from .app import app, register_command
|
||||||
|
|
||||||
|
__all__ = ["app", "register_command"]
|
||||||
95
src/fastapi_toolsets/cli/app.py
Normal file
95
src/fastapi_toolsets/cli/app.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""Main CLI application."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from .commands import fixtures
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="fastapi-utils",
|
||||||
|
help="CLI utilities for FastAPI projects.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register built-in commands
|
||||||
|
app.add_typer(fixtures.app, name="fixtures")
|
||||||
|
|
||||||
|
|
||||||
|
def register_command(command: typer.Typer, name: str) -> None:
|
||||||
|
"""Register a custom command group.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Typer app for the command group
|
||||||
|
name: Name for the command group
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# In your project's cli.py:
|
||||||
|
import typer
|
||||||
|
from fastapi_toolsets.cli import app, register_command
|
||||||
|
|
||||||
|
my_commands = typer.Typer()
|
||||||
|
|
||||||
|
@my_commands.command()
|
||||||
|
def seed():
|
||||||
|
'''Seed the database.'''
|
||||||
|
...
|
||||||
|
|
||||||
|
register_command(my_commands, "db")
|
||||||
|
# Now available as: fastapi-utils db seed
|
||||||
|
"""
|
||||||
|
app.add_typer(command, name=name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback()
|
||||||
|
def main(
|
||||||
|
ctx: typer.Context,
|
||||||
|
config: Annotated[
|
||||||
|
Path | None,
|
||||||
|
typer.Option(
|
||||||
|
"--config",
|
||||||
|
"-c",
|
||||||
|
help="Path to project config file (Python module with fixtures registry).",
|
||||||
|
envvar="FASTAPI_TOOLSETS_CONFIG",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""FastAPI utilities CLI."""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
|
||||||
|
if config:
|
||||||
|
ctx.obj["config_path"] = config
|
||||||
|
# Load the config module
|
||||||
|
config_module = _load_module_from_path(config)
|
||||||
|
ctx.obj["config_module"] = config_module
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module_from_path(path: Path) -> object:
|
||||||
|
"""Load a Python module from a file path.
|
||||||
|
|
||||||
|
Handles both absolute and relative imports by adding the config's
|
||||||
|
parent directory to sys.path temporarily.
|
||||||
|
"""
|
||||||
|
path = path.resolve()
|
||||||
|
|
||||||
|
# Add the parent directory to sys.path to support relative imports
|
||||||
|
parent_dir = str(path.parent.parent) # Go up two levels (e.g., from app/cli_config.py to project root)
|
||||||
|
if parent_dir not in sys.path:
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
# Also add immediate parent for direct module imports
|
||||||
|
immediate_parent = str(path.parent)
|
||||||
|
if immediate_parent not in sys.path:
|
||||||
|
sys.path.insert(0, immediate_parent)
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("config", path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise typer.BadParameter(f"Cannot load module from {path}")
|
||||||
|
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["config"] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
1
src/fastapi_toolsets/cli/commands/__init__.py
Normal file
1
src/fastapi_toolsets/cli/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Built-in CLI commands."""
|
||||||
211
src/fastapi_toolsets/cli/commands/fixtures.py
Normal file
211
src/fastapi_toolsets/cli/commands/fixtures.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
"""Fixture management commands."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from ...fixtures import Context, FixtureRegistry, LoadStrategy, load_fixtures_by_context
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="fixtures",
|
||||||
|
help="Manage database fixtures.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_registry(ctx: typer.Context) -> FixtureRegistry:
|
||||||
|
"""Get fixture registry from context."""
|
||||||
|
config = ctx.obj.get("config_module") if ctx.obj else None
|
||||||
|
if config is None:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
"No config provided. Use --config to specify a config file with a 'fixtures' registry."
|
||||||
|
)
|
||||||
|
|
||||||
|
registry = getattr(config, "fixtures", None)
|
||||||
|
if registry is None:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
"Config module must have a 'fixtures' attribute (FixtureRegistry instance)."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(registry, FixtureRegistry):
|
||||||
|
raise typer.BadParameter(
|
||||||
|
f"'fixtures' must be a FixtureRegistry instance, got {type(registry).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def _get_db_context(ctx: typer.Context):
|
||||||
|
"""Get database context manager from config."""
|
||||||
|
config = ctx.obj.get("config_module") if ctx.obj else None
|
||||||
|
if config is None:
|
||||||
|
raise typer.BadParameter("No config provided.")
|
||||||
|
|
||||||
|
get_db_context = getattr(config, "get_db_context", None)
|
||||||
|
if get_db_context is None:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
"Config module must have a 'get_db_context' function."
|
||||||
|
)
|
||||||
|
|
||||||
|
return get_db_context
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("list")
|
||||||
|
def list_fixtures(
|
||||||
|
ctx: typer.Context,
|
||||||
|
context: Annotated[
|
||||||
|
str | None,
|
||||||
|
typer.Option("--context", "-c", help="Filter by context (base, production, development, testing)."),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""List all registered fixtures."""
|
||||||
|
registry = _get_registry(ctx)
|
||||||
|
|
||||||
|
if context:
|
||||||
|
fixtures = registry.get_by_context(context)
|
||||||
|
else:
|
||||||
|
fixtures = registry.get_all()
|
||||||
|
|
||||||
|
if not fixtures:
|
||||||
|
typer.echo("No fixtures found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
typer.echo(f"\n{'Name':<30} {'Contexts':<30} {'Dependencies'}")
|
||||||
|
typer.echo("-" * 80)
|
||||||
|
|
||||||
|
for fixture in fixtures:
|
||||||
|
contexts = ", ".join(fixture.contexts)
|
||||||
|
deps = ", ".join(fixture.depends_on) if fixture.depends_on else "-"
|
||||||
|
typer.echo(f"{fixture.name:<30} {contexts:<30} {deps}")
|
||||||
|
|
||||||
|
typer.echo(f"\nTotal: {len(fixtures)} fixture(s)")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("graph")
|
||||||
|
def show_graph(
|
||||||
|
ctx: typer.Context,
|
||||||
|
fixture_name: Annotated[
|
||||||
|
str | None,
|
||||||
|
typer.Argument(help="Show dependencies for a specific fixture."),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Show fixture dependency graph."""
|
||||||
|
registry = _get_registry(ctx)
|
||||||
|
|
||||||
|
if fixture_name:
|
||||||
|
try:
|
||||||
|
order = registry.resolve_dependencies(fixture_name)
|
||||||
|
typer.echo(f"\nDependency chain for '{fixture_name}':\n")
|
||||||
|
for i, name in enumerate(order):
|
||||||
|
indent = " " * i
|
||||||
|
arrow = "└─> " if i > 0 else ""
|
||||||
|
typer.echo(f"{indent}{arrow}{name}")
|
||||||
|
except KeyError:
|
||||||
|
typer.echo(f"Fixture '{fixture_name}' not found.", err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
else:
|
||||||
|
# Show full graph
|
||||||
|
fixtures = registry.get_all()
|
||||||
|
|
||||||
|
typer.echo("\nFixture Dependency Graph:\n")
|
||||||
|
for fixture in fixtures:
|
||||||
|
deps = f" -> [{', '.join(fixture.depends_on)}]" if fixture.depends_on else ""
|
||||||
|
typer.echo(f" {fixture.name}{deps}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("load")
|
||||||
|
def load(
|
||||||
|
ctx: typer.Context,
|
||||||
|
contexts: Annotated[
|
||||||
|
list[str] | None,
|
||||||
|
typer.Argument(help="Contexts to load (base, production, development, testing)."),
|
||||||
|
] = None,
|
||||||
|
strategy: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option("--strategy", "-s", help="Load strategy: merge, insert, skip_existing."),
|
||||||
|
] = "merge",
|
||||||
|
dry_run: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option("--dry-run", "-n", help="Show what would be loaded without loading."),
|
||||||
|
] = False,
|
||||||
|
) -> None:
|
||||||
|
"""Load fixtures into the database."""
|
||||||
|
registry = _get_registry(ctx)
|
||||||
|
get_db_context = _get_db_context(ctx)
|
||||||
|
|
||||||
|
# Parse contexts
|
||||||
|
if contexts:
|
||||||
|
context_list = contexts
|
||||||
|
else:
|
||||||
|
context_list = [Context.BASE]
|
||||||
|
|
||||||
|
# Parse strategy
|
||||||
|
try:
|
||||||
|
load_strategy = LoadStrategy(strategy)
|
||||||
|
except ValueError:
|
||||||
|
typer.echo(f"Invalid strategy: {strategy}. Use: merge, insert, skip_existing", err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Resolve what will be loaded
|
||||||
|
ordered = registry.resolve_context_dependencies(*context_list)
|
||||||
|
|
||||||
|
if not ordered:
|
||||||
|
typer.echo("No fixtures to load for the specified context(s).")
|
||||||
|
return
|
||||||
|
|
||||||
|
typer.echo(f"\nFixtures to load ({load_strategy.value} strategy):")
|
||||||
|
for name in ordered:
|
||||||
|
fixture = registry.get(name)
|
||||||
|
instances = list(fixture.func())
|
||||||
|
model_name = type(instances[0]).__name__ if instances else "?"
|
||||||
|
typer.echo(f" - {name}: {len(instances)} {model_name}(s)")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
typer.echo("\n[Dry run - no changes made]")
|
||||||
|
return
|
||||||
|
|
||||||
|
typer.echo("\nLoading...")
|
||||||
|
|
||||||
|
async def do_load():
|
||||||
|
async with get_db_context() as session:
|
||||||
|
result = await load_fixtures_by_context(
|
||||||
|
session, registry, *context_list, strategy=load_strategy
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = asyncio.run(do_load())
|
||||||
|
|
||||||
|
total = sum(len(items) for items in result.values())
|
||||||
|
typer.echo(f"\nLoaded {total} record(s) successfully.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("show")
|
||||||
|
def show_fixture(
|
||||||
|
ctx: typer.Context,
|
||||||
|
name: Annotated[str, typer.Argument(help="Fixture name to show.")],
|
||||||
|
) -> None:
|
||||||
|
"""Show details of a specific fixture."""
|
||||||
|
registry = _get_registry(ctx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fixture = registry.get(name)
|
||||||
|
except KeyError:
|
||||||
|
typer.echo(f"Fixture '{name}' not found.", err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
typer.echo(f"\nFixture: {fixture.name}")
|
||||||
|
typer.echo(f"Contexts: {', '.join(fixture.contexts)}")
|
||||||
|
typer.echo(f"Dependencies: {', '.join(fixture.depends_on) if fixture.depends_on else 'None'}")
|
||||||
|
|
||||||
|
# Show instances
|
||||||
|
instances = list(fixture.func())
|
||||||
|
if instances:
|
||||||
|
model_name = type(instances[0]).__name__
|
||||||
|
typer.echo(f"\nInstances ({len(instances)} {model_name}):")
|
||||||
|
for instance in instances[:10]: # Limit to 10
|
||||||
|
typer.echo(f" - {instance!r}")
|
||||||
|
if len(instances) > 10:
|
||||||
|
typer.echo(f" ... and {len(instances) - 10} more")
|
||||||
|
else:
|
||||||
|
typer.echo("\nNo instances (empty fixture)")
|
||||||
378
src/fastapi_toolsets/crud.py
Normal file
378
src/fastapi_toolsets/crud.py
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
"""Generic async CRUD operations for SQLAlchemy models."""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any, ClassVar, Generic, Self, TypeVar, cast
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import and_, func, select
|
||||||
|
from sqlalchemy import delete as sql_delete
|
||||||
|
from sqlalchemy.dialects.postgresql import insert
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from sqlalchemy.sql.roles import WhereHavingRole
|
||||||
|
|
||||||
|
from .db import get_transaction
|
||||||
|
from .exceptions import NotFoundError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AsyncCrud",
|
||||||
|
"CrudFactory",
|
||||||
|
]
|
||||||
|
|
||||||
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncCrud(Generic[ModelType]):
|
||||||
|
"""Generic async CRUD operations for SQLAlchemy models.
|
||||||
|
|
||||||
|
Subclass this and set the `model` class variable, or use `CrudFactory`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
class UserCrud(AsyncCrud[User]):
|
||||||
|
model = User
|
||||||
|
|
||||||
|
# Or use the factory:
|
||||||
|
UserCrud = CrudFactory(User)
|
||||||
|
|
||||||
|
# Then use it:
|
||||||
|
user = await UserCrud.get(session, [User.id == 1])
|
||||||
|
users = await UserCrud.get_multi(session, limit=10)
|
||||||
|
"""
|
||||||
|
|
||||||
|
model: ClassVar[type[DeclarativeBase]]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
obj: BaseModel,
|
||||||
|
) -> ModelType:
|
||||||
|
"""Create a new record in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
obj: Pydantic model with data to create
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created model instance
|
||||||
|
"""
|
||||||
|
async with get_transaction(session):
|
||||||
|
db_model = cls.model(**obj.model_dump())
|
||||||
|
session.add(db_model)
|
||||||
|
await session.refresh(db_model)
|
||||||
|
return cast(ModelType, db_model)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
filters: list[Any],
|
||||||
|
*,
|
||||||
|
with_for_update: bool = False,
|
||||||
|
load_options: list[Any] | None = None,
|
||||||
|
) -> ModelType:
|
||||||
|
"""Get exactly one record. Raises NotFoundError if not found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
with_for_update: Lock the row for update
|
||||||
|
load_options: SQLAlchemy loader options (e.g., selectinload)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotFoundError: If no record found
|
||||||
|
MultipleResultsFound: If more than one record found
|
||||||
|
"""
|
||||||
|
q = select(cls.model).where(and_(*filters))
|
||||||
|
if load_options:
|
||||||
|
q = q.options(*load_options)
|
||||||
|
if with_for_update:
|
||||||
|
q = q.with_for_update()
|
||||||
|
result = await session.execute(q)
|
||||||
|
item = result.unique().scalar_one_or_none()
|
||||||
|
if not item:
|
||||||
|
raise NotFoundError()
|
||||||
|
return cast(ModelType, item)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def first(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
filters: list[Any] | None = None,
|
||||||
|
*,
|
||||||
|
load_options: list[Any] | None = None,
|
||||||
|
) -> ModelType | None:
|
||||||
|
"""Get the first matching record, or None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
load_options: SQLAlchemy loader options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model instance or None
|
||||||
|
"""
|
||||||
|
q = select(cls.model)
|
||||||
|
if filters:
|
||||||
|
q = q.where(and_(*filters))
|
||||||
|
if load_options:
|
||||||
|
q = q.options(*load_options)
|
||||||
|
result = await session.execute(q)
|
||||||
|
return cast(ModelType | None, result.unique().scalars().first())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_multi(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
filters: list[Any] | None = None,
|
||||||
|
load_options: list[Any] | None = None,
|
||||||
|
order_by: Any | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
offset: int | None = None,
|
||||||
|
) -> Sequence[ModelType]:
|
||||||
|
"""Get multiple records from the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
load_options: SQLAlchemy loader options
|
||||||
|
order_by: Column or list of columns to order by
|
||||||
|
limit: Max number of rows to return
|
||||||
|
offset: Rows to skip
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of model instances
|
||||||
|
"""
|
||||||
|
q = select(cls.model)
|
||||||
|
if filters:
|
||||||
|
q = q.where(and_(*filters))
|
||||||
|
if load_options:
|
||||||
|
q = q.options(*load_options)
|
||||||
|
if order_by is not None:
|
||||||
|
q = q.order_by(order_by)
|
||||||
|
if offset is not None:
|
||||||
|
q = q.offset(offset)
|
||||||
|
if limit is not None:
|
||||||
|
q = q.limit(limit)
|
||||||
|
result = await session.execute(q)
|
||||||
|
return cast(Sequence[ModelType], result.unique().scalars().all())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def update(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
obj: BaseModel,
|
||||||
|
filters: list[Any],
|
||||||
|
*,
|
||||||
|
exclude_unset: bool = True,
|
||||||
|
exclude_none: bool = False,
|
||||||
|
) -> ModelType:
|
||||||
|
"""Update a record in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
obj: Pydantic model with update data
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
exclude_unset: Exclude fields not explicitly set in the schema
|
||||||
|
exclude_none: Exclude fields with None value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated model instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotFoundError: If no record found
|
||||||
|
"""
|
||||||
|
async with get_transaction(session):
|
||||||
|
db_model = await cls.get(session=session, filters=filters)
|
||||||
|
values = obj.model_dump(
|
||||||
|
exclude_unset=exclude_unset, exclude_none=exclude_none
|
||||||
|
)
|
||||||
|
for key, value in values.items():
|
||||||
|
setattr(db_model, key, value)
|
||||||
|
await session.refresh(db_model)
|
||||||
|
return db_model
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def upsert(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
obj: BaseModel,
|
||||||
|
index_elements: list[str],
|
||||||
|
*,
|
||||||
|
set_: BaseModel | None = None,
|
||||||
|
where: WhereHavingRole | None = None,
|
||||||
|
) -> ModelType | None:
|
||||||
|
"""Create or update a record (PostgreSQL only).
|
||||||
|
|
||||||
|
Uses INSERT ... ON CONFLICT for atomic upsert.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
obj: Pydantic model with data
|
||||||
|
index_elements: Columns for ON CONFLICT (unique constraint)
|
||||||
|
set_: Pydantic model for ON CONFLICT DO UPDATE SET
|
||||||
|
where: WHERE clause for ON CONFLICT DO UPDATE
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model instance
|
||||||
|
"""
|
||||||
|
async with get_transaction(session):
|
||||||
|
values = obj.model_dump(exclude_unset=True)
|
||||||
|
q = insert(cls.model).values(**values)
|
||||||
|
if set_:
|
||||||
|
q = q.on_conflict_do_update(
|
||||||
|
index_elements=index_elements,
|
||||||
|
set_=set_.model_dump(exclude_unset=True),
|
||||||
|
where=where,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
q = q.on_conflict_do_nothing(index_elements=index_elements)
|
||||||
|
q = q.returning(cls.model)
|
||||||
|
result = await session.execute(q)
|
||||||
|
try:
|
||||||
|
db_model = result.unique().scalar_one()
|
||||||
|
except NoResultFound:
|
||||||
|
db_model = await cls.first(
|
||||||
|
session=session,
|
||||||
|
filters=[getattr(cls.model, k) == v for k, v in values.items()],
|
||||||
|
)
|
||||||
|
return cast(ModelType | None, db_model)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
filters: list[Any],
|
||||||
|
) -> bool:
|
||||||
|
"""Delete records from the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deletion was executed
|
||||||
|
"""
|
||||||
|
async with get_transaction(session):
|
||||||
|
q = sql_delete(cls.model).where(and_(*filters))
|
||||||
|
await session.execute(q)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def count(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
filters: list[Any] | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""Count records matching the filters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of matching records
|
||||||
|
"""
|
||||||
|
q = select(func.count()).select_from(cls.model)
|
||||||
|
if filters:
|
||||||
|
q = q.where(and_(*filters))
|
||||||
|
result = await session.execute(q)
|
||||||
|
return result.scalar_one()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def exists(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
filters: list[Any],
|
||||||
|
) -> bool:
|
||||||
|
"""Check if a record exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if at least one record matches
|
||||||
|
"""
|
||||||
|
q = select(cls.model).where(and_(*filters)).exists().select()
|
||||||
|
result = await session.execute(q)
|
||||||
|
return bool(result.scalar())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def paginate(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
filters: list[Any] | None = None,
|
||||||
|
load_options: list[Any] | None = None,
|
||||||
|
order_by: Any | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
items_per_page: int = 20,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Get paginated results with metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: DB async session
|
||||||
|
filters: List of SQLAlchemy filter conditions
|
||||||
|
load_options: SQLAlchemy loader options
|
||||||
|
order_by: Column or list of columns to order by
|
||||||
|
page: Page number (1-indexed)
|
||||||
|
items_per_page: Number of items per page
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with 'data' and 'pagination' keys
|
||||||
|
"""
|
||||||
|
filters = filters or []
|
||||||
|
offset = (page - 1) * items_per_page
|
||||||
|
|
||||||
|
items = await cls.get_multi(
|
||||||
|
session,
|
||||||
|
filters=filters,
|
||||||
|
load_options=load_options,
|
||||||
|
order_by=order_by,
|
||||||
|
limit=items_per_page,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_count = await cls.count(session, filters=filters)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": items,
|
||||||
|
"pagination": {
|
||||||
|
"total_count": total_count,
|
||||||
|
"items_per_page": items_per_page,
|
||||||
|
"page": page,
|
||||||
|
"has_more": page * items_per_page < total_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def CrudFactory(
|
||||||
|
model: type[ModelType],
|
||||||
|
) -> type[AsyncCrud[ModelType]]:
|
||||||
|
"""Create a CRUD class for a specific model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: SQLAlchemy model class
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AsyncCrud subclass bound to the model
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
|
from myapp.models import User, Post
|
||||||
|
|
||||||
|
UserCrud = CrudFactory(User)
|
||||||
|
PostCrud = CrudFactory(Post)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
user = await UserCrud.get(session, [User.id == 1])
|
||||||
|
posts = await PostCrud.get_multi(session, filters=[Post.user_id == user.id])
|
||||||
|
"""
|
||||||
|
cls = type(f"Async{model.__name__}Crud", (AsyncCrud,), {"model": model})
|
||||||
|
return cast(type[AsyncCrud[ModelType]], cls)
|
||||||
175
src/fastapi_toolsets/db.py
Normal file
175
src/fastapi_toolsets/db.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
"""Database utilities: sessions, transactions, and locks."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator, Callable
|
||||||
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LockMode",
|
||||||
|
"create_db_context",
|
||||||
|
"create_db_dependency",
|
||||||
|
"lock_tables",
|
||||||
|
"get_transaction",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_db_dependency(
|
||||||
|
session_maker: async_sessionmaker[AsyncSession],
|
||||||
|
) -> Callable[[], AsyncGenerator[AsyncSession, None]]:
|
||||||
|
"""Create a FastAPI dependency for database sessions.
|
||||||
|
|
||||||
|
Creates a dependency function that yields a session and auto-commits
|
||||||
|
if a transaction is active when the request completes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_maker: Async session factory from create_session_factory()
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An async generator function usable with FastAPI's Depends()
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||||
|
from fastapi_toolsets.db import create_db_dependency
|
||||||
|
|
||||||
|
engine = create_async_engine("postgresql+asyncpg://...")
|
||||||
|
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
get_db = create_db_dependency(SessionLocal)
|
||||||
|
|
||||||
|
@app.get("/users")
|
||||||
|
async def list_users(session: AsyncSession = Depends(get_db)):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with session_maker() as session:
|
||||||
|
yield session
|
||||||
|
if session.in_transaction():
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return get_db
|
||||||
|
|
||||||
|
|
||||||
|
def create_db_context(
|
||||||
|
session_maker: async_sessionmaker[AsyncSession],
|
||||||
|
) -> Callable[[], AbstractAsyncContextManager[AsyncSession]]:
|
||||||
|
"""Create a context manager for database sessions.
|
||||||
|
|
||||||
|
Creates a context manager for use outside of FastAPI request handlers,
|
||||||
|
such as in background tasks, CLI commands, or tests.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_maker: Async session factory from create_session_factory()
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An async context manager function
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||||
|
from fastapi_toolsets.db import create_db_context
|
||||||
|
|
||||||
|
engine = create_async_engine("postgresql+asyncpg://...")
|
||||||
|
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
get_db_context = create_db_context(SessionLocal)
|
||||||
|
|
||||||
|
async def background_task():
|
||||||
|
async with get_db_context() as session:
|
||||||
|
user = await UserCrud.get(session, [User.id == 1])
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
get_db = create_db_dependency(session_maker)
|
||||||
|
return asynccontextmanager(get_db)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_transaction(
|
||||||
|
session: AsyncSession,
|
||||||
|
) -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""Get a transaction context, handling nested transactions.
|
||||||
|
|
||||||
|
If already in a transaction, creates a savepoint (nested transaction).
|
||||||
|
Otherwise, starts a new transaction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: AsyncSession instance
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
The session within the transaction context
|
||||||
|
|
||||||
|
Example:
|
||||||
|
async with get_transaction(session):
|
||||||
|
session.add(model)
|
||||||
|
# Auto-commits on exit, rolls back on exception
|
||||||
|
"""
|
||||||
|
if session.in_transaction():
|
||||||
|
async with session.begin_nested():
|
||||||
|
yield session
|
||||||
|
else:
|
||||||
|
async with session.begin():
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
class LockMode(str, Enum):
|
||||||
|
"""PostgreSQL table lock modes.
|
||||||
|
|
||||||
|
See: https://www.postgresql.org/docs/current/explicit-locking.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
ACCESS_SHARE = "ACCESS SHARE"
|
||||||
|
ROW_SHARE = "ROW SHARE"
|
||||||
|
ROW_EXCLUSIVE = "ROW EXCLUSIVE"
|
||||||
|
SHARE_UPDATE_EXCLUSIVE = "SHARE UPDATE EXCLUSIVE"
|
||||||
|
SHARE = "SHARE"
|
||||||
|
SHARE_ROW_EXCLUSIVE = "SHARE ROW EXCLUSIVE"
|
||||||
|
EXCLUSIVE = "EXCLUSIVE"
|
||||||
|
ACCESS_EXCLUSIVE = "ACCESS EXCLUSIVE"
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lock_tables(
|
||||||
|
session: AsyncSession,
|
||||||
|
tables: list[type[DeclarativeBase]],
|
||||||
|
*,
|
||||||
|
mode: LockMode = LockMode.SHARE_UPDATE_EXCLUSIVE,
|
||||||
|
timeout: str = "5s",
|
||||||
|
) -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""Lock PostgreSQL tables for the duration of a transaction.
|
||||||
|
|
||||||
|
Acquires table-level locks that are held until the transaction ends.
|
||||||
|
Useful for preventing concurrent modifications during critical operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: AsyncSession instance
|
||||||
|
tables: List of SQLAlchemy model classes to lock
|
||||||
|
mode: Lock mode (default: SHARE UPDATE EXCLUSIVE)
|
||||||
|
timeout: Lock timeout (default: "5s")
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
The session with locked tables
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SQLAlchemyError: If lock cannot be acquired within timeout
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi_toolsets.db import lock_tables, LockMode
|
||||||
|
|
||||||
|
async with lock_tables(session, [User, Account]):
|
||||||
|
# Tables are locked with SHARE UPDATE EXCLUSIVE mode
|
||||||
|
user = await UserCrud.get(session, [User.id == 1])
|
||||||
|
user.balance += 100
|
||||||
|
|
||||||
|
# With custom lock mode
|
||||||
|
async with lock_tables(session, [Order], mode=LockMode.EXCLUSIVE):
|
||||||
|
# Exclusive lock - no other transactions can access
|
||||||
|
await process_order(session, order_id)
|
||||||
|
"""
|
||||||
|
table_names = ",".join(table.__tablename__ for table in tables)
|
||||||
|
|
||||||
|
async with get_transaction(session):
|
||||||
|
await session.execute(text(f"SET LOCAL lock_timeout='{timeout}'"))
|
||||||
|
await session.execute(text(f"LOCK {table_names} IN {mode.value} MODE"))
|
||||||
|
yield session
|
||||||
19
src/fastapi_toolsets/exceptions/__init__.py
Normal file
19
src/fastapi_toolsets/exceptions/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from .exceptions import (
|
||||||
|
ApiException,
|
||||||
|
ConflictError,
|
||||||
|
ForbiddenError,
|
||||||
|
NotFoundError,
|
||||||
|
UnauthorizedError,
|
||||||
|
generate_error_responses,
|
||||||
|
)
|
||||||
|
from .handler import init_exceptions_handlers
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"init_exceptions_handlers",
|
||||||
|
"generate_error_responses",
|
||||||
|
"ApiException",
|
||||||
|
"ConflictError",
|
||||||
|
"ForbiddenError",
|
||||||
|
"NotFoundError",
|
||||||
|
"UnauthorizedError",
|
||||||
|
]
|
||||||
166
src/fastapi_toolsets/exceptions/exceptions.py
Normal file
166
src/fastapi_toolsets/exceptions/exceptions.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""Custom exceptions with standardized API error responses."""
|
||||||
|
|
||||||
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
from ..schemas import ApiError, ErrorResponse, ResponseStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ApiException(Exception):
|
||||||
|
"""Base exception for API errors with structured response.
|
||||||
|
|
||||||
|
Subclass this to create custom API exceptions with consistent error format.
|
||||||
|
The exception handler will use api_error to generate the response.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
class CustomError(ApiException):
|
||||||
|
api_error = ApiError(
|
||||||
|
code=400,
|
||||||
|
msg="Bad Request",
|
||||||
|
desc="The request was invalid.",
|
||||||
|
err_code="CUSTOM-400",
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
api_error: ClassVar[ApiError]
|
||||||
|
|
||||||
|
def __init__(self, detail: str | None = None):
|
||||||
|
"""Initialize the exception.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detail: Optional override for the error message
|
||||||
|
"""
|
||||||
|
super().__init__(detail or self.api_error.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedError(ApiException):
|
||||||
|
"""HTTP 401 - User is not authenticated."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=401,
|
||||||
|
msg="Unauthorized",
|
||||||
|
desc="Authentication credentials were missing or invalid.",
|
||||||
|
err_code="AUTH-401",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenError(ApiException):
|
||||||
|
"""HTTP 403 - User lacks required permissions."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=403,
|
||||||
|
msg="Forbidden",
|
||||||
|
desc="You do not have permission to access this resource.",
|
||||||
|
err_code="AUTH-403",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(ApiException):
|
||||||
|
"""HTTP 404 - Resource not found."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=404,
|
||||||
|
msg="Not Found",
|
||||||
|
desc="The requested resource was not found.",
|
||||||
|
err_code="RES-404",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictError(ApiException):
|
||||||
|
"""HTTP 409 - Resource conflict."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=409,
|
||||||
|
msg="Conflict",
|
||||||
|
desc="The request conflicts with the current state of the resource.",
|
||||||
|
err_code="RES-409",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InsufficientRolesError(ForbiddenError):
|
||||||
|
"""User does not have the required roles."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=403,
|
||||||
|
msg="Insufficient Roles",
|
||||||
|
desc="You do not have the required roles to access this resource.",
|
||||||
|
err_code="RBAC-403",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, required_roles: list[str], user_roles: set[str] | None = None):
|
||||||
|
self.required_roles = required_roles
|
||||||
|
self.user_roles = user_roles
|
||||||
|
|
||||||
|
desc = f"Required roles: {', '.join(required_roles)}"
|
||||||
|
if user_roles is not None:
|
||||||
|
desc += f". User has: {', '.join(user_roles) if user_roles else 'no roles'}"
|
||||||
|
|
||||||
|
super().__init__(desc)
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(NotFoundError):
|
||||||
|
"""User was not found."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=404,
|
||||||
|
msg="User Not Found",
|
||||||
|
desc="The requested user was not found.",
|
||||||
|
err_code="USER-404",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleNotFoundError(NotFoundError):
|
||||||
|
"""Role was not found."""
|
||||||
|
|
||||||
|
api_error = ApiError(
|
||||||
|
code=404,
|
||||||
|
msg="Role Not Found",
|
||||||
|
desc="The requested role was not found.",
|
||||||
|
err_code="ROLE-404",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_error_responses(
|
||||||
|
*errors: type[ApiException],
|
||||||
|
) -> dict[int | str, dict[str, Any]]:
|
||||||
|
"""Generate OpenAPI response documentation for exceptions.
|
||||||
|
|
||||||
|
Use this to document possible error responses for an endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*errors: Exception classes that inherit from ApiException
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict suitable for FastAPI's responses parameter
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi_toolsets.exceptions import generate_error_responses, UnauthorizedError, ForbiddenError
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/admin",
|
||||||
|
responses=generate_error_responses(UnauthorizedError, ForbiddenError)
|
||||||
|
)
|
||||||
|
async def admin_endpoint():
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
responses: dict[int | str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
for error in errors:
|
||||||
|
api_error = error.api_error
|
||||||
|
|
||||||
|
responses[api_error.code] = {
|
||||||
|
"model": ErrorResponse,
|
||||||
|
"description": api_error.msg,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"data": None,
|
||||||
|
"status": ResponseStatus.FAIL.value,
|
||||||
|
"message": api_error.msg,
|
||||||
|
"description": api_error.desc,
|
||||||
|
"error_code": api_error.err_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses
|
||||||
169
src/fastapi_toolsets/exceptions/handler.py
Normal file
169
src/fastapi_toolsets/exceptions/handler.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
"""Exception handlers for FastAPI applications."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request, Response, status
|
||||||
|
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
||||||
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from ..schemas import ResponseStatus
|
||||||
|
from .exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
def init_exceptions_handlers(app: FastAPI) -> FastAPI:
|
||||||
|
_register_exception_handlers(app)
|
||||||
|
app.openapi = lambda: _custom_openapi(app) # type: ignore[method-assign]
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def _register_exception_handlers(app: FastAPI) -> None:
|
||||||
|
"""Register all exception handlers on a FastAPI application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.exception_handler(ApiException)
|
||||||
|
async def api_exception_handler(request: Request, exc: ApiException) -> Response:
|
||||||
|
"""Handle custom API exceptions with structured response."""
|
||||||
|
api_error = exc.api_error
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=api_error.code,
|
||||||
|
content={
|
||||||
|
"data": None,
|
||||||
|
"status": ResponseStatus.FAIL.value,
|
||||||
|
"message": api_error.msg,
|
||||||
|
"description": api_error.desc,
|
||||||
|
"error_code": api_error.err_code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def request_validation_handler(
|
||||||
|
request: Request, exc: RequestValidationError
|
||||||
|
) -> Response:
|
||||||
|
"""Handle Pydantic request validation errors (422)."""
|
||||||
|
return _format_validation_error(exc)
|
||||||
|
|
||||||
|
@app.exception_handler(ResponseValidationError)
|
||||||
|
async def response_validation_handler(
|
||||||
|
request: Request, exc: ResponseValidationError
|
||||||
|
) -> Response:
|
||||||
|
"""Handle Pydantic response validation errors (422)."""
|
||||||
|
return _format_validation_error(exc)
|
||||||
|
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def generic_exception_handler(request: Request, exc: Exception) -> Response:
|
||||||
|
"""Handle all unhandled exceptions with a generic 500 response."""
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content={
|
||||||
|
"data": None,
|
||||||
|
"status": ResponseStatus.FAIL.value,
|
||||||
|
"message": "Internal Server Error",
|
||||||
|
"description": "An unexpected error occurred. Please try again later.",
|
||||||
|
"error_code": "SERVER-500",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_validation_error(
|
||||||
|
exc: RequestValidationError | ResponseValidationError,
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Format validation errors into a structured response."""
|
||||||
|
errors = exc.errors()
|
||||||
|
formatted_errors = []
|
||||||
|
|
||||||
|
for error in errors:
|
||||||
|
field_path = ".".join(
|
||||||
|
str(loc)
|
||||||
|
for loc in error["loc"]
|
||||||
|
if loc not in ("body", "query", "path", "header", "cookie")
|
||||||
|
)
|
||||||
|
formatted_errors.append(
|
||||||
|
{
|
||||||
|
"field": field_path or "root",
|
||||||
|
"message": error.get("msg", ""),
|
||||||
|
"type": error.get("type", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
content={
|
||||||
|
"data": {"errors": formatted_errors},
|
||||||
|
"status": ResponseStatus.FAIL.value,
|
||||||
|
"message": "Validation Error",
|
||||||
|
"description": f"{len(formatted_errors)} validation error(s) detected",
|
||||||
|
"error_code": "VAL-422",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _custom_openapi(app: FastAPI) -> dict[str, Any]:
|
||||||
|
"""Generate custom OpenAPI schema with standardized error format.
|
||||||
|
|
||||||
|
Replaces default 422 validation error responses with the custom format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OpenAPI schema dict
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app) # Automatically sets custom OpenAPI
|
||||||
|
"""
|
||||||
|
if app.openapi_schema:
|
||||||
|
return app.openapi_schema
|
||||||
|
|
||||||
|
openapi_schema = get_openapi(
|
||||||
|
title=app.title,
|
||||||
|
version=app.version,
|
||||||
|
openapi_version=app.openapi_version,
|
||||||
|
description=app.description,
|
||||||
|
routes=app.routes,
|
||||||
|
)
|
||||||
|
|
||||||
|
for path_data in openapi_schema.get("paths", {}).values():
|
||||||
|
for operation in path_data.values():
|
||||||
|
if isinstance(operation, dict) and "responses" in operation:
|
||||||
|
if "422" in operation["responses"]:
|
||||||
|
operation["responses"]["422"] = {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"data": {
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"field": "field_name",
|
||||||
|
"message": "value is not valid",
|
||||||
|
"type": "value_error",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": ResponseStatus.FAIL.value,
|
||||||
|
"message": "Validation Error",
|
||||||
|
"description": "1 validation error(s) detected",
|
||||||
|
"error_code": "VAL-422",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.openapi_schema = openapi_schema
|
||||||
|
return app.openapi_schema
|
||||||
17
src/fastapi_toolsets/fixtures/__init__.py
Normal file
17
src/fastapi_toolsets/fixtures/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from .fixtures import (
|
||||||
|
Context,
|
||||||
|
FixtureRegistry,
|
||||||
|
LoadStrategy,
|
||||||
|
load_fixtures,
|
||||||
|
load_fixtures_by_context,
|
||||||
|
)
|
||||||
|
from .pytest_plugin import register_fixtures
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Context",
|
||||||
|
"FixtureRegistry",
|
||||||
|
"LoadStrategy",
|
||||||
|
"load_fixtures",
|
||||||
|
"load_fixtures_by_context",
|
||||||
|
"register_fixtures",
|
||||||
|
]
|
||||||
321
src/fastapi_toolsets/fixtures/fixtures.py
Normal file
321
src/fastapi_toolsets/fixtures/fixtures.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
"""Fixture system with dependency management and context support."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
from ..db import get_transaction
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LoadStrategy(str, Enum):
|
||||||
|
"""Strategy for loading fixtures into the database."""
|
||||||
|
|
||||||
|
INSERT = "insert"
|
||||||
|
"""Insert new records. Fails if record already exists."""
|
||||||
|
|
||||||
|
MERGE = "merge"
|
||||||
|
"""Insert or update based on primary key (SQLAlchemy merge)."""
|
||||||
|
|
||||||
|
SKIP_EXISTING = "skip_existing"
|
||||||
|
"""Insert only if record doesn't exist (based on primary key)."""
|
||||||
|
|
||||||
|
|
||||||
|
class Context(str, Enum):
|
||||||
|
"""Predefined fixture contexts."""
|
||||||
|
|
||||||
|
BASE = "base"
|
||||||
|
"""Base fixtures loaded in all environments."""
|
||||||
|
|
||||||
|
PRODUCTION = "production"
|
||||||
|
"""Production-only fixtures."""
|
||||||
|
|
||||||
|
DEVELOPMENT = "development"
|
||||||
|
"""Development fixtures."""
|
||||||
|
|
||||||
|
TESTING = "testing"
|
||||||
|
"""Test fixtures."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Fixture:
|
||||||
|
"""A fixture definition with metadata."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
func: Callable[[], Sequence[DeclarativeBase]]
|
||||||
|
depends_on: list[str] = field(default_factory=list)
|
||||||
|
contexts: list[str] = field(default_factory=lambda: [Context.BASE])
|
||||||
|
|
||||||
|
|
||||||
|
class FixtureRegistry:
|
||||||
|
"""Registry for managing fixtures with dependencies.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from fastapi_toolsets.fixtures import FixtureRegistry, Context
|
||||||
|
|
||||||
|
fixtures = FixtureRegistry()
|
||||||
|
|
||||||
|
@fixtures.register
|
||||||
|
def roles():
|
||||||
|
return [
|
||||||
|
Role(id=1, name="admin"),
|
||||||
|
Role(id=2, name="user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
@fixtures.register(depends_on=["roles"])
|
||||||
|
def users():
|
||||||
|
return [
|
||||||
|
User(id=1, username="admin", role_id=1),
|
||||||
|
]
|
||||||
|
|
||||||
|
@fixtures.register(depends_on=["users"], contexts=[Context.TESTING])
|
||||||
|
def test_data():
|
||||||
|
return [
|
||||||
|
Post(id=1, title="Test", user_id=1),
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._fixtures: dict[str, Fixture] = {}
|
||||||
|
|
||||||
|
def register(
|
||||||
|
self,
|
||||||
|
func: Callable[[], Sequence[DeclarativeBase]] | None = None,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
depends_on: list[str] | None = None,
|
||||||
|
contexts: list[str | Context] | None = None,
|
||||||
|
) -> Callable[..., Any]:
|
||||||
|
"""Register a fixture function.
|
||||||
|
|
||||||
|
Can be used as a decorator with or without arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: Fixture function returning list of model instances
|
||||||
|
name: Fixture name (defaults to function name)
|
||||||
|
depends_on: List of fixture names this depends on
|
||||||
|
contexts: List of contexts this fixture belongs to
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@fixtures.register
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
@fixtures.register(depends_on=["roles"], contexts=[Context.TESTING])
|
||||||
|
def test_users():
|
||||||
|
return [User(id=1, username="test", role_id=1)]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(
|
||||||
|
fn: Callable[[], Sequence[DeclarativeBase]],
|
||||||
|
) -> Callable[[], Sequence[DeclarativeBase]]:
|
||||||
|
fixture_name = name or cast(Any, fn).__name__
|
||||||
|
fixture_contexts = [
|
||||||
|
c.value if isinstance(c, Context) else c
|
||||||
|
for c in (contexts or [Context.BASE])
|
||||||
|
]
|
||||||
|
|
||||||
|
self._fixtures[fixture_name] = Fixture(
|
||||||
|
name=fixture_name,
|
||||||
|
func=fn,
|
||||||
|
depends_on=depends_on or [],
|
||||||
|
contexts=fixture_contexts,
|
||||||
|
)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
if func is not None:
|
||||||
|
return decorator(func)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def get(self, name: str) -> Fixture:
|
||||||
|
"""Get a fixture by name."""
|
||||||
|
if name not in self._fixtures:
|
||||||
|
raise KeyError(f"Fixture '{name}' not found")
|
||||||
|
return self._fixtures[name]
|
||||||
|
|
||||||
|
def get_all(self) -> list[Fixture]:
|
||||||
|
"""Get all registered fixtures."""
|
||||||
|
return list(self._fixtures.values())
|
||||||
|
|
||||||
|
def get_by_context(self, *contexts: str | Context) -> list[Fixture]:
|
||||||
|
"""Get fixtures for specific contexts."""
|
||||||
|
context_values = {c.value if isinstance(c, Context) else c for c in contexts}
|
||||||
|
return [f for f in self._fixtures.values() if set(f.contexts) & context_values]
|
||||||
|
|
||||||
|
def resolve_dependencies(self, *names: str) -> list[str]:
|
||||||
|
"""Resolve fixture dependencies in topological order.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*names: Fixture names to resolve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of fixture names in load order (dependencies first)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If a fixture is not found
|
||||||
|
ValueError: If circular dependency detected
|
||||||
|
"""
|
||||||
|
resolved: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
visiting: set[str] = set()
|
||||||
|
|
||||||
|
def visit(name: str) -> None:
|
||||||
|
if name in resolved:
|
||||||
|
return
|
||||||
|
if name in visiting:
|
||||||
|
raise ValueError(f"Circular dependency detected: {name}")
|
||||||
|
|
||||||
|
visiting.add(name)
|
||||||
|
fixture = self.get(name)
|
||||||
|
|
||||||
|
for dep in fixture.depends_on:
|
||||||
|
visit(dep)
|
||||||
|
|
||||||
|
visiting.remove(name)
|
||||||
|
resolved.append(name)
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
visit(name)
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
def resolve_context_dependencies(self, *contexts: str | Context) -> list[str]:
|
||||||
|
"""Resolve all fixtures for contexts with dependencies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*contexts: Contexts to load
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of fixture names in load order
|
||||||
|
"""
|
||||||
|
context_fixtures = self.get_by_context(*contexts)
|
||||||
|
names = [f.name for f in context_fixtures]
|
||||||
|
|
||||||
|
all_deps: set[str] = set()
|
||||||
|
for name in names:
|
||||||
|
deps = self.resolve_dependencies(name)
|
||||||
|
all_deps.update(deps)
|
||||||
|
|
||||||
|
return self.resolve_dependencies(*all_deps)
|
||||||
|
|
||||||
|
|
||||||
|
async def load_fixtures(
|
||||||
|
session: AsyncSession,
|
||||||
|
registry: FixtureRegistry,
|
||||||
|
*names: str,
|
||||||
|
strategy: LoadStrategy = LoadStrategy.MERGE,
|
||||||
|
) -> dict[str, list[DeclarativeBase]]:
|
||||||
|
"""Load specific fixtures by name with dependencies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
registry: Fixture registry
|
||||||
|
*names: Fixture names to load (dependencies auto-resolved)
|
||||||
|
strategy: How to handle existing records
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping fixture names to loaded instances
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Loads 'roles' first (dependency), then 'users'
|
||||||
|
result = await load_fixtures(session, fixtures, "users")
|
||||||
|
print(result["users"]) # [User(...), ...]
|
||||||
|
"""
|
||||||
|
ordered = registry.resolve_dependencies(*names)
|
||||||
|
return await _load_ordered(session, registry, ordered, strategy)
|
||||||
|
|
||||||
|
|
||||||
|
async def load_fixtures_by_context(
|
||||||
|
session: AsyncSession,
|
||||||
|
registry: FixtureRegistry,
|
||||||
|
*contexts: str | Context,
|
||||||
|
strategy: LoadStrategy = LoadStrategy.MERGE,
|
||||||
|
) -> dict[str, list[DeclarativeBase]]:
|
||||||
|
"""Load all fixtures for specific contexts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
registry: Fixture registry
|
||||||
|
*contexts: Contexts to load (e.g., Context.BASE, Context.TESTING)
|
||||||
|
strategy: How to handle existing records
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping fixture names to loaded instances
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Load base + testing fixtures
|
||||||
|
await load_fixtures_by_context(
|
||||||
|
session, fixtures,
|
||||||
|
Context.BASE, Context.TESTING
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
ordered = registry.resolve_context_dependencies(*contexts)
|
||||||
|
return await _load_ordered(session, registry, ordered, strategy)
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_ordered(
|
||||||
|
session: AsyncSession,
|
||||||
|
registry: FixtureRegistry,
|
||||||
|
ordered_names: list[str],
|
||||||
|
strategy: LoadStrategy,
|
||||||
|
) -> dict[str, list[DeclarativeBase]]:
|
||||||
|
"""Load fixtures in order."""
|
||||||
|
results: dict[str, list[DeclarativeBase]] = {}
|
||||||
|
|
||||||
|
for name in ordered_names:
|
||||||
|
fixture = registry.get(name)
|
||||||
|
instances = list(fixture.func())
|
||||||
|
|
||||||
|
if not instances:
|
||||||
|
results[name] = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
model_name = type(instances[0]).__name__
|
||||||
|
loaded: list[DeclarativeBase] = []
|
||||||
|
|
||||||
|
async with get_transaction(session):
|
||||||
|
for instance in instances:
|
||||||
|
if strategy == LoadStrategy.INSERT:
|
||||||
|
session.add(instance)
|
||||||
|
loaded.append(instance)
|
||||||
|
|
||||||
|
elif strategy == LoadStrategy.MERGE:
|
||||||
|
merged = await session.merge(instance)
|
||||||
|
loaded.append(merged)
|
||||||
|
|
||||||
|
elif strategy == LoadStrategy.SKIP_EXISTING:
|
||||||
|
pk = _get_primary_key(instance)
|
||||||
|
if pk is not None:
|
||||||
|
existing = await session.get(type(instance), pk)
|
||||||
|
if existing is None:
|
||||||
|
session.add(instance)
|
||||||
|
loaded.append(instance)
|
||||||
|
else:
|
||||||
|
session.add(instance)
|
||||||
|
loaded.append(instance)
|
||||||
|
|
||||||
|
results[name] = loaded
|
||||||
|
logger.info(f"Loaded fixture '{name}': {len(loaded)} {model_name}(s)")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _get_primary_key(instance: DeclarativeBase) -> Any | None:
|
||||||
|
"""Get the primary key value of a model instance."""
|
||||||
|
mapper = instance.__class__.__mapper__
|
||||||
|
pk_cols = mapper.primary_key
|
||||||
|
|
||||||
|
if len(pk_cols) == 1:
|
||||||
|
return getattr(instance, pk_cols[0].name, None)
|
||||||
|
|
||||||
|
pk_values = tuple(getattr(instance, col.name, None) for col in pk_cols)
|
||||||
|
if all(v is not None for v in pk_values):
|
||||||
|
return pk_values
|
||||||
|
return None
|
||||||
205
src/fastapi_toolsets/fixtures/pytest_plugin.py
Normal file
205
src/fastapi_toolsets/fixtures/pytest_plugin.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""Pytest plugin for using FixtureRegistry fixtures in tests.
|
||||||
|
|
||||||
|
This module provides utilities to automatically generate pytest fixtures
|
||||||
|
from your FixtureRegistry, with proper dependency resolution.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# conftest.py
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from app.fixtures import fixtures # Your FixtureRegistry
|
||||||
|
from app.models import Base
|
||||||
|
from fastapi_toolsets.pytest_plugin import register_fixtures
|
||||||
|
|
||||||
|
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def engine():
|
||||||
|
engine = create_async_engine(DATABASE_URL)
|
||||||
|
yield engine
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def db_session(engine):
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
session = session_factory()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
|
||||||
|
# Automatically generate pytest fixtures from registry
|
||||||
|
# Creates: fixture_roles, fixture_users, fixture_posts, etc.
|
||||||
|
register_fixtures(fixtures, globals())
|
||||||
|
|
||||||
|
Usage in tests:
|
||||||
|
# test_users.py
|
||||||
|
async def test_user_count(db_session, fixture_users):
|
||||||
|
# fixture_users automatically loads fixture_roles first (if dependency)
|
||||||
|
# and returns the list of User models
|
||||||
|
assert len(fixture_users) > 0
|
||||||
|
|
||||||
|
async def test_user_role(db_session, fixture_users):
|
||||||
|
user = fixture_users[0]
|
||||||
|
assert user.role_id is not None
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
from ..db import get_transaction
|
||||||
|
from .fixtures import FixtureRegistry, LoadStrategy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def register_fixtures(
|
||||||
|
registry: FixtureRegistry,
|
||||||
|
namespace: dict[str, Any],
|
||||||
|
*,
|
||||||
|
prefix: str = "fixture_",
|
||||||
|
session_fixture: str = "db_session",
|
||||||
|
strategy: LoadStrategy = LoadStrategy.MERGE,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Register pytest fixtures from a FixtureRegistry.
|
||||||
|
|
||||||
|
Automatically creates pytest fixtures for each fixture in the registry.
|
||||||
|
Dependencies are resolved via pytest fixture dependencies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry: The FixtureRegistry containing fixtures
|
||||||
|
namespace: The module's globals() dict to add fixtures to
|
||||||
|
prefix: Prefix for generated fixture names (default: "fixture_")
|
||||||
|
session_fixture: Name of the db session fixture (default: "db_session")
|
||||||
|
strategy: Loading strategy for fixtures (default: MERGE)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of created fixture names
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# conftest.py
|
||||||
|
from app.fixtures import fixtures
|
||||||
|
from fastapi_toolsets.pytest_plugin import register_fixtures
|
||||||
|
|
||||||
|
register_fixtures(fixtures, globals())
|
||||||
|
|
||||||
|
# Creates fixtures like:
|
||||||
|
# - fixture_roles
|
||||||
|
# - fixture_users (depends on fixture_roles if users depends on roles)
|
||||||
|
# - fixture_posts (depends on fixture_users if posts depends on users)
|
||||||
|
"""
|
||||||
|
created_fixtures: list[str] = []
|
||||||
|
|
||||||
|
for fixture in registry.get_all():
|
||||||
|
fixture_name = f"{prefix}{fixture.name}"
|
||||||
|
|
||||||
|
# Build list of pytest fixture dependencies
|
||||||
|
pytest_deps = [session_fixture]
|
||||||
|
for dep in fixture.depends_on:
|
||||||
|
pytest_deps.append(f"{prefix}{dep}")
|
||||||
|
|
||||||
|
# Create the fixture function
|
||||||
|
fixture_func = _create_fixture_function(
|
||||||
|
registry=registry,
|
||||||
|
fixture_name=fixture.name,
|
||||||
|
dependencies=pytest_deps,
|
||||||
|
strategy=strategy,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply pytest.fixture decorator
|
||||||
|
decorated = pytest.fixture(fixture_func)
|
||||||
|
|
||||||
|
# Add to namespace
|
||||||
|
namespace[fixture_name] = decorated
|
||||||
|
created_fixtures.append(fixture_name)
|
||||||
|
|
||||||
|
return created_fixtures
|
||||||
|
|
||||||
|
|
||||||
|
def _create_fixture_function(
|
||||||
|
registry: FixtureRegistry,
|
||||||
|
fixture_name: str,
|
||||||
|
dependencies: list[str],
|
||||||
|
strategy: LoadStrategy,
|
||||||
|
) -> Callable[..., Any]:
|
||||||
|
"""Create a fixture function with the correct signature.
|
||||||
|
|
||||||
|
The function signature must include all dependencies as parameters
|
||||||
|
for pytest to resolve them correctly.
|
||||||
|
"""
|
||||||
|
# Get the fixture definition
|
||||||
|
fixture_def = registry.get(fixture_name)
|
||||||
|
|
||||||
|
# Build the function dynamically with correct parameters
|
||||||
|
# We need the session as first param, then all dependencies
|
||||||
|
async def fixture_func(**kwargs: Any) -> Sequence[DeclarativeBase]:
|
||||||
|
# Get session from kwargs (first dependency)
|
||||||
|
session: AsyncSession = kwargs[dependencies[0]]
|
||||||
|
|
||||||
|
# Load the fixture data
|
||||||
|
instances = list(fixture_def.func())
|
||||||
|
|
||||||
|
if not instances:
|
||||||
|
return []
|
||||||
|
|
||||||
|
loaded: list[DeclarativeBase] = []
|
||||||
|
|
||||||
|
async with get_transaction(session):
|
||||||
|
for instance in instances:
|
||||||
|
if strategy == LoadStrategy.INSERT:
|
||||||
|
session.add(instance)
|
||||||
|
loaded.append(instance)
|
||||||
|
elif strategy == LoadStrategy.MERGE:
|
||||||
|
merged = await session.merge(instance)
|
||||||
|
loaded.append(merged)
|
||||||
|
elif strategy == LoadStrategy.SKIP_EXISTING:
|
||||||
|
pk = _get_primary_key(instance)
|
||||||
|
if pk is not None:
|
||||||
|
existing = await session.get(type(instance), pk)
|
||||||
|
if existing is None:
|
||||||
|
session.add(instance)
|
||||||
|
loaded.append(instance)
|
||||||
|
else:
|
||||||
|
loaded.append(existing)
|
||||||
|
else:
|
||||||
|
session.add(instance)
|
||||||
|
loaded.append(instance)
|
||||||
|
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
# Update function signature to include dependencies
|
||||||
|
# This is needed for pytest to inject the right fixtures
|
||||||
|
params = ", ".join(dependencies)
|
||||||
|
code = f"async def {fixture_name}_fixture({params}):\n return await _impl({', '.join(f'{d}={d}' for d in dependencies)})"
|
||||||
|
|
||||||
|
local_ns: dict[str, Any] = {"_impl": fixture_func}
|
||||||
|
exec(code, local_ns) # noqa: S102
|
||||||
|
|
||||||
|
created_func = local_ns[f"{fixture_name}_fixture"]
|
||||||
|
created_func.__doc__ = f"Load {fixture_name} fixture data."
|
||||||
|
|
||||||
|
return created_func
|
||||||
|
|
||||||
|
|
||||||
|
def _get_primary_key(instance: DeclarativeBase) -> Any | None:
|
||||||
|
"""Get the primary key value of a model instance."""
|
||||||
|
mapper = instance.__class__.__mapper__
|
||||||
|
pk_cols = mapper.primary_key
|
||||||
|
|
||||||
|
if len(pk_cols) == 1:
|
||||||
|
return getattr(instance, pk_cols[0].name, None)
|
||||||
|
|
||||||
|
pk_values = tuple(getattr(instance, col.name, None) for col in pk_cols)
|
||||||
|
if all(v is not None for v in pk_values):
|
||||||
|
return pk_values
|
||||||
|
return None
|
||||||
0
src/fastapi_toolsets/py.typed
Normal file
0
src/fastapi_toolsets/py.typed
Normal file
116
src/fastapi_toolsets/schemas.py
Normal file
116
src/fastapi_toolsets/schemas.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Base Pydantic schemas for API responses."""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import ClassVar, Generic, TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApiError",
|
||||||
|
"ErrorResponse",
|
||||||
|
"Pagination",
|
||||||
|
"PaginatedResponse",
|
||||||
|
"Response",
|
||||||
|
"ResponseStatus",
|
||||||
|
]
|
||||||
|
|
||||||
|
DataT = TypeVar("DataT")
|
||||||
|
|
||||||
|
|
||||||
|
class PydanticBase(BaseModel):
|
||||||
|
"""Base class for all Pydantic models with common configuration."""
|
||||||
|
|
||||||
|
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||||
|
from_attributes=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseStatus(str, Enum):
|
||||||
|
"""Standard API response status."""
|
||||||
|
|
||||||
|
SUCCESS = "SUCCESS"
|
||||||
|
FAIL = "FAIL"
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(PydanticBase):
|
||||||
|
"""Structured API error definition.
|
||||||
|
|
||||||
|
Used to define standard error responses with consistent format.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
code: HTTP status code
|
||||||
|
msg: Short error message
|
||||||
|
desc: Detailed error description
|
||||||
|
err_code: Application-specific error code (e.g., "AUTH-401")
|
||||||
|
"""
|
||||||
|
|
||||||
|
code: int
|
||||||
|
msg: str
|
||||||
|
desc: str
|
||||||
|
err_code: str
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResponse(PydanticBase):
|
||||||
|
"""Base response structure for all API responses.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
status: SUCCESS or FAIL
|
||||||
|
message: Human-readable message
|
||||||
|
error_code: Error code if status is FAIL, None otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
status: ResponseStatus = ResponseStatus.SUCCESS
|
||||||
|
message: str = "Success"
|
||||||
|
error_code: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Response(BaseResponse, Generic[DataT]):
|
||||||
|
"""Generic API response with data payload.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Response[UserRead](data=user, message="User retrieved")
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: DataT | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponse(BaseResponse):
|
||||||
|
"""Error response with additional description field.
|
||||||
|
|
||||||
|
Used for error responses that need more context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
status: ResponseStatus = ResponseStatus.FAIL
|
||||||
|
description: str | None = None
|
||||||
|
data: None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Pagination(PydanticBase):
|
||||||
|
"""Pagination metadata for list responses.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
total_count: Total number of items across all pages
|
||||||
|
items_per_page: Number of items per page
|
||||||
|
page: Current page number (1-indexed)
|
||||||
|
has_more: Whether there are more pages
|
||||||
|
"""
|
||||||
|
|
||||||
|
total_count: int
|
||||||
|
items_per_page: int
|
||||||
|
page: int
|
||||||
|
has_more: bool
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedResponse(BaseResponse, Generic[DataT]):
|
||||||
|
"""Paginated API response for list endpoints.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
PaginatedResponse[UserRead](
|
||||||
|
data=users,
|
||||||
|
pagination=Pagination(total_count=100, items_per_page=10, page=1, has_more=True)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: list[DataT]
|
||||||
|
pagination: Pagination
|
||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for fastapi-utils package."""
|
||||||
199
tests/conftest.py
Normal file
199
tests/conftest.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
"""Shared pytest fixtures for fastapi-utils tests."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import ForeignKey, String
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
|
|
||||||
|
# PostgreSQL connection URL from environment or default for local development
|
||||||
|
DATABASE_URL = os.getenv(
|
||||||
|
"TEST_DATABASE_URL",
|
||||||
|
"postgresql+asyncpg://postgres:postgres@localhost:5432/fastapi_toolsets_test",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Test Models
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
"""Base class for test models."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Role(Base):
|
||||||
|
"""Test role model."""
|
||||||
|
|
||||||
|
__tablename__ = "roles"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), unique=True)
|
||||||
|
|
||||||
|
users: Mapped[list["User"]] = relationship(back_populates="role")
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
"""Test user model."""
|
||||||
|
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
username: Mapped[str] = mapped_column(String(50), unique=True)
|
||||||
|
email: Mapped[str] = mapped_column(String(100), unique=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(default=True)
|
||||||
|
role_id: Mapped[int | None] = mapped_column(ForeignKey("roles.id"), nullable=True)
|
||||||
|
|
||||||
|
role: Mapped[Role | None] = relationship(back_populates="users")
|
||||||
|
|
||||||
|
|
||||||
|
class Post(Base):
|
||||||
|
"""Test post model."""
|
||||||
|
|
||||||
|
__tablename__ = "posts"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
title: Mapped[str] = mapped_column(String(200))
|
||||||
|
content: Mapped[str] = mapped_column(String(1000), default="")
|
||||||
|
is_published: Mapped[bool] = mapped_column(default=False)
|
||||||
|
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Test Schemas
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class RoleCreate(BaseModel):
|
||||||
|
"""Schema for creating a role."""
|
||||||
|
|
||||||
|
id: int | None = None
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUpdate(BaseModel):
|
||||||
|
"""Schema for updating a role."""
|
||||||
|
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
"""Schema for creating a user."""
|
||||||
|
|
||||||
|
id: int | None = None
|
||||||
|
username: str
|
||||||
|
email: str
|
||||||
|
is_active: bool = True
|
||||||
|
role_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
"""Schema for updating a user."""
|
||||||
|
|
||||||
|
username: str | None = None
|
||||||
|
email: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
role_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PostCreate(BaseModel):
|
||||||
|
"""Schema for creating a post."""
|
||||||
|
|
||||||
|
id: int | None = None
|
||||||
|
title: str
|
||||||
|
content: str = ""
|
||||||
|
is_published: bool = False
|
||||||
|
author_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class PostUpdate(BaseModel):
|
||||||
|
"""Schema for updating a post."""
|
||||||
|
|
||||||
|
title: str | None = None
|
||||||
|
content: str | None = None
|
||||||
|
is_published: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CRUD Classes
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
RoleCrud = CrudFactory(Role)
|
||||||
|
UserCrud = CrudFactory(User)
|
||||||
|
PostCrud = CrudFactory(Post)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Fixtures
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def anyio_backend():
|
||||||
|
"""Use asyncio for async tests."""
|
||||||
|
return "asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
async def engine():
|
||||||
|
"""Create a PostgreSQL test database engine."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
yield engine
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
async def db_session(engine) -> AsyncSession:
|
||||||
|
"""Create a test database session with tables.
|
||||||
|
|
||||||
|
Creates all tables before the test and drops them after.
|
||||||
|
Each test gets a clean database state.
|
||||||
|
"""
|
||||||
|
# Create tables
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
# Create session
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
session = session_factory()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
# Drop tables after test
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_role_data() -> RoleCreate:
|
||||||
|
"""Sample role creation data."""
|
||||||
|
return RoleCreate(name="admin")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_user_data() -> UserCreate:
|
||||||
|
"""Sample user creation data."""
|
||||||
|
return UserCreate(
|
||||||
|
username="testuser",
|
||||||
|
email="test@example.com",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_post_data() -> PostCreate:
|
||||||
|
"""Sample post creation data."""
|
||||||
|
return PostCreate(
|
||||||
|
title="Test Post",
|
||||||
|
content="Test content",
|
||||||
|
is_published=True,
|
||||||
|
author_id=1,
|
||||||
|
)
|
||||||
475
tests/test_crud.py
Normal file
475
tests/test_crud.py
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
"""Tests for fastapi_toolsets.crud module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from fastapi_toolsets.crud import AsyncCrud, CrudFactory
|
||||||
|
from fastapi_toolsets.exceptions import NotFoundError
|
||||||
|
|
||||||
|
from .conftest import (
|
||||||
|
Role,
|
||||||
|
RoleCreate,
|
||||||
|
RoleCrud,
|
||||||
|
RoleUpdate,
|
||||||
|
User,
|
||||||
|
UserCreate,
|
||||||
|
UserCrud,
|
||||||
|
UserUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudFactory:
|
||||||
|
"""Tests for CrudFactory."""
|
||||||
|
|
||||||
|
def test_creates_crud_class(self):
|
||||||
|
"""CrudFactory creates a properly configured CRUD class."""
|
||||||
|
crud = CrudFactory(User)
|
||||||
|
assert issubclass(crud, AsyncCrud)
|
||||||
|
assert crud.model is User
|
||||||
|
|
||||||
|
def test_creates_unique_classes(self):
|
||||||
|
"""Each call creates a unique class."""
|
||||||
|
crud1 = CrudFactory(User)
|
||||||
|
crud2 = CrudFactory(User)
|
||||||
|
assert crud1 is not crud2
|
||||||
|
|
||||||
|
def test_class_name_includes_model(self):
|
||||||
|
"""Generated class name includes model name."""
|
||||||
|
crud = CrudFactory(User)
|
||||||
|
assert "User" in crud.__name__
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudCreate:
|
||||||
|
"""Tests for CRUD create operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_single_record(self, db_session: AsyncSession):
|
||||||
|
"""Create a single record."""
|
||||||
|
data = RoleCreate(name="admin")
|
||||||
|
role = await RoleCrud.create(db_session, data)
|
||||||
|
|
||||||
|
assert role.id is not None
|
||||||
|
assert role.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_with_relationship(self, db_session: AsyncSession):
|
||||||
|
"""Create records with foreign key relationships."""
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="user"))
|
||||||
|
user_data = UserCreate(
|
||||||
|
username="john",
|
||||||
|
email="john@example.com",
|
||||||
|
role_id=role.id,
|
||||||
|
)
|
||||||
|
user = await UserCrud.create(db_session, user_data)
|
||||||
|
|
||||||
|
assert user.role_id == role.id
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_with_defaults(self, db_session: AsyncSession):
|
||||||
|
"""Create uses model defaults."""
|
||||||
|
user_data = UserCreate(username="jane", email="jane@example.com")
|
||||||
|
user = await UserCrud.create(db_session, user_data)
|
||||||
|
|
||||||
|
assert user.is_active is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudGet:
|
||||||
|
"""Tests for CRUD get operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_existing_record(self, db_session: AsyncSession):
|
||||||
|
"""Get an existing record by filter."""
|
||||||
|
created = await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
fetched = await RoleCrud.get(db_session, [Role.id == created.id])
|
||||||
|
|
||||||
|
assert fetched.id == created.id
|
||||||
|
assert fetched.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_raises_not_found(self, db_session: AsyncSession):
|
||||||
|
"""Get raises NotFoundError for missing records."""
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
await RoleCrud.get(db_session, [Role.id == 99999])
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_with_multiple_filters(self, db_session: AsyncSession):
|
||||||
|
"""Get with multiple filter conditions."""
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="active", email="active@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="inactive", email="inactive@test.com", is_active=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = await UserCrud.get(
|
||||||
|
db_session,
|
||||||
|
[User.username == "active", User.is_active == True], # noqa: E712
|
||||||
|
)
|
||||||
|
assert user.username == "active"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudFirst:
|
||||||
|
"""Tests for CRUD first operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_first_returns_record(self, db_session: AsyncSession):
|
||||||
|
"""First returns the first matching record."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
role = await RoleCrud.first(db_session, [Role.name == "admin"])
|
||||||
|
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_first_returns_none_when_not_found(self, db_session: AsyncSession):
|
||||||
|
"""First returns None for missing records."""
|
||||||
|
role = await RoleCrud.first(db_session, [Role.name == "nonexistent"])
|
||||||
|
assert role is None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_first_without_filters(self, db_session: AsyncSession):
|
||||||
|
"""First without filters returns any record."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="role1"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="role2"))
|
||||||
|
|
||||||
|
role = await RoleCrud.first(db_session)
|
||||||
|
assert role is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudGetMulti:
|
||||||
|
"""Tests for CRUD get_multi operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_multi_returns_all(self, db_session: AsyncSession):
|
||||||
|
"""Get multiple records."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="user"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="guest"))
|
||||||
|
|
||||||
|
roles = await RoleCrud.get_multi(db_session)
|
||||||
|
assert len(roles) == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_multi_with_filters(self, db_session: AsyncSession):
|
||||||
|
"""Get multiple with filter."""
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="active1", email="a1@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="active2", email="a2@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="inactive", email="i@test.com", is_active=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
active_users = await UserCrud.get_multi(
|
||||||
|
db_session,
|
||||||
|
filters=[User.is_active == True], # noqa: E712
|
||||||
|
)
|
||||||
|
assert len(active_users) == 2
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_multi_with_limit(self, db_session: AsyncSession):
|
||||||
|
"""Get multiple with limit."""
|
||||||
|
for i in range(5):
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name=f"role{i}"))
|
||||||
|
|
||||||
|
roles = await RoleCrud.get_multi(db_session, limit=3)
|
||||||
|
assert len(roles) == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_multi_with_offset(self, db_session: AsyncSession):
|
||||||
|
"""Get multiple with offset."""
|
||||||
|
for i in range(5):
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name=f"role{i}"))
|
||||||
|
|
||||||
|
roles = await RoleCrud.get_multi(db_session, offset=2)
|
||||||
|
assert len(roles) == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_multi_with_order_by(self, db_session: AsyncSession):
|
||||||
|
"""Get multiple with ordering."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="charlie"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="alpha"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="bravo"))
|
||||||
|
|
||||||
|
roles = await RoleCrud.get_multi(db_session, order_by=Role.name)
|
||||||
|
names = [r.name for r in roles]
|
||||||
|
assert names == ["alpha", "bravo", "charlie"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudUpdate:
|
||||||
|
"""Tests for CRUD update operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_update_record(self, db_session: AsyncSession):
|
||||||
|
"""Update an existing record."""
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="old_name"))
|
||||||
|
updated = await RoleCrud.update(
|
||||||
|
db_session,
|
||||||
|
RoleUpdate(name="new_name"),
|
||||||
|
[Role.id == role.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated.name == "new_name"
|
||||||
|
assert updated.id == role.id
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_update_raises_not_found(self, db_session: AsyncSession):
|
||||||
|
"""Update raises NotFoundError for missing records."""
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
await RoleCrud.update(
|
||||||
|
db_session,
|
||||||
|
RoleUpdate(name="new"),
|
||||||
|
[Role.id == 99999],
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_update_excludes_unset(self, db_session: AsyncSession):
|
||||||
|
"""Update excludes unset fields by default."""
|
||||||
|
user = await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="john", email="john@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await UserCrud.update(
|
||||||
|
db_session,
|
||||||
|
UserUpdate(username="johnny"),
|
||||||
|
[User.id == user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated.username == "johnny"
|
||||||
|
assert updated.email == "john@test.com"
|
||||||
|
assert updated.is_active is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudDelete:
|
||||||
|
"""Tests for CRUD delete operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_record(self, db_session: AsyncSession):
|
||||||
|
"""Delete an existing record."""
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="to_delete"))
|
||||||
|
result = await RoleCrud.delete(db_session, [Role.id == role.id])
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert await RoleCrud.first(db_session, [Role.id == role.id]) is None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_multiple_records(self, db_session: AsyncSession):
|
||||||
|
"""Delete multiple records with filter."""
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="u1", email="u1@test.com", is_active=False),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="u2", email="u2@test.com", is_active=False),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="u3", email="u3@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
await UserCrud.delete(db_session, [User.is_active == False]) # noqa: E712
|
||||||
|
remaining = await UserCrud.get_multi(db_session)
|
||||||
|
assert len(remaining) == 1
|
||||||
|
assert remaining[0].username == "u3"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudExists:
|
||||||
|
"""Tests for CRUD exists operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_exists_returns_true(self, db_session: AsyncSession):
|
||||||
|
"""Exists returns True for existing records."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
assert await RoleCrud.exists(db_session, [Role.name == "admin"]) is True
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_exists_returns_false(self, db_session: AsyncSession):
|
||||||
|
"""Exists returns False for missing records."""
|
||||||
|
assert await RoleCrud.exists(db_session, [Role.name == "nonexistent"]) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudCount:
|
||||||
|
"""Tests for CRUD count operations."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_count_all(self, db_session: AsyncSession):
|
||||||
|
"""Count all records."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="role1"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="role2"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="role3"))
|
||||||
|
|
||||||
|
count = await RoleCrud.count(db_session)
|
||||||
|
assert count == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_count_with_filter(self, db_session: AsyncSession):
|
||||||
|
"""Count records with filter."""
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="a1", email="a1@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="a2", email="a2@test.com", is_active=True),
|
||||||
|
)
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="i1", email="i1@test.com", is_active=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
active_count = await UserCrud.count(
|
||||||
|
db_session,
|
||||||
|
filters=[User.is_active == True], # noqa: E712
|
||||||
|
)
|
||||||
|
assert active_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudUpsert:
|
||||||
|
"""Tests for CRUD upsert operations (PostgreSQL-specific)."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_upsert_insert_new_record(self, db_session: AsyncSession):
|
||||||
|
"""Upsert inserts a new record when it doesn't exist."""
|
||||||
|
data = RoleCreate(id=1, name="upsert_new")
|
||||||
|
role = await RoleCrud.upsert(
|
||||||
|
db_session,
|
||||||
|
data,
|
||||||
|
index_elements=["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "upsert_new"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_upsert_update_existing_record(self, db_session: AsyncSession):
|
||||||
|
"""Upsert updates an existing record."""
|
||||||
|
# First insert
|
||||||
|
data = RoleCreate(id=100, name="original_name")
|
||||||
|
await RoleCrud.upsert(db_session, data, index_elements=["id"])
|
||||||
|
|
||||||
|
# Upsert with update
|
||||||
|
updated_data = RoleCreate(id=100, name="updated_name")
|
||||||
|
role = await RoleCrud.upsert(
|
||||||
|
db_session,
|
||||||
|
updated_data,
|
||||||
|
index_elements=["id"],
|
||||||
|
set_=RoleUpdate(name="updated_name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "updated_name"
|
||||||
|
|
||||||
|
# Verify only one record exists
|
||||||
|
count = await RoleCrud.count(db_session, [Role.id == 100])
|
||||||
|
assert count == 1
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_upsert_do_nothing_on_conflict(self, db_session: AsyncSession):
|
||||||
|
"""Upsert does nothing on conflict when set_ is not provided."""
|
||||||
|
# First insert
|
||||||
|
data = RoleCreate(id=200, name="do_nothing_original")
|
||||||
|
await RoleCrud.upsert(db_session, data, index_elements=["id"])
|
||||||
|
|
||||||
|
# Upsert without set_ (do nothing)
|
||||||
|
conflict_data = RoleCreate(id=200, name="do_nothing_conflict")
|
||||||
|
await RoleCrud.upsert(db_session, conflict_data, index_elements=["id"])
|
||||||
|
|
||||||
|
# Original value should be preserved
|
||||||
|
role = await RoleCrud.first(db_session, [Role.id == 200])
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "do_nothing_original"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_upsert_with_unique_constraint(self, db_session: AsyncSession):
|
||||||
|
"""Upsert works with unique constraint columns."""
|
||||||
|
# Insert first role
|
||||||
|
data1 = RoleCreate(name="unique_role")
|
||||||
|
await RoleCrud.upsert(db_session, data1, index_elements=["name"])
|
||||||
|
|
||||||
|
# Upsert with same name - should update (or do nothing)
|
||||||
|
data2 = RoleCreate(name="unique_role")
|
||||||
|
role = await RoleCrud.upsert(db_session, data2, index_elements=["name"])
|
||||||
|
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "unique_role"
|
||||||
|
|
||||||
|
# Should still be only one record
|
||||||
|
count = await RoleCrud.count(db_session, [Role.name == "unique_role"])
|
||||||
|
assert count == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrudPaginate:
|
||||||
|
"""Tests for CRUD pagination."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_paginate_first_page(self, db_session: AsyncSession):
|
||||||
|
"""Paginate returns first page."""
|
||||||
|
for i in range(25):
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name=f"role{i:02d}"))
|
||||||
|
|
||||||
|
result = await RoleCrud.paginate(db_session, page=1, items_per_page=10)
|
||||||
|
|
||||||
|
assert len(result["data"]) == 10
|
||||||
|
assert result["pagination"]["total_count"] == 25
|
||||||
|
assert result["pagination"]["page"] == 1
|
||||||
|
assert result["pagination"]["items_per_page"] == 10
|
||||||
|
assert result["pagination"]["has_more"] is True
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_paginate_last_page(self, db_session: AsyncSession):
|
||||||
|
"""Paginate returns last page with has_more=False."""
|
||||||
|
for i in range(25):
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name=f"role{i:02d}"))
|
||||||
|
|
||||||
|
result = await RoleCrud.paginate(db_session, page=3, items_per_page=10)
|
||||||
|
|
||||||
|
assert len(result["data"]) == 5
|
||||||
|
assert result["pagination"]["has_more"] is False
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_paginate_with_filters(self, db_session: AsyncSession):
|
||||||
|
"""Paginate with filter conditions."""
|
||||||
|
for i in range(10):
|
||||||
|
await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(
|
||||||
|
username=f"user{i}",
|
||||||
|
email=f"user{i}@test.com",
|
||||||
|
is_active=i % 2 == 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await UserCrud.paginate(
|
||||||
|
db_session,
|
||||||
|
filters=[User.is_active == True], # noqa: E712
|
||||||
|
page=1,
|
||||||
|
items_per_page=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["pagination"]["total_count"] == 5
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_paginate_with_ordering(self, db_session: AsyncSession):
|
||||||
|
"""Paginate with custom ordering."""
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="charlie"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="alpha"))
|
||||||
|
await RoleCrud.create(db_session, RoleCreate(name="bravo"))
|
||||||
|
|
||||||
|
result = await RoleCrud.paginate(
|
||||||
|
db_session,
|
||||||
|
order_by=Role.name,
|
||||||
|
page=1,
|
||||||
|
items_per_page=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
names = [r.name for r in result["data"]]
|
||||||
|
assert names == ["alpha", "bravo", "charlie"]
|
||||||
243
tests/test_db.py
Normal file
243
tests/test_db.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
"""Tests for fastapi_toolsets.db module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from fastapi_toolsets.db import (
|
||||||
|
LockMode,
|
||||||
|
create_db_context,
|
||||||
|
create_db_dependency,
|
||||||
|
get_transaction,
|
||||||
|
lock_tables,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .conftest import DATABASE_URL, Base, Role, RoleCrud, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateDbDependency:
|
||||||
|
"""Tests for create_db_dependency."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_yields_session(self):
|
||||||
|
"""Dependency yields a valid session."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
get_db = create_db_dependency(session_factory)
|
||||||
|
|
||||||
|
async for session in get_db():
|
||||||
|
assert isinstance(session, AsyncSession)
|
||||||
|
break
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_auto_commits_transaction(self):
|
||||||
|
"""Dependency auto-commits if transaction is active."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_db = create_db_dependency(session_factory)
|
||||||
|
|
||||||
|
async for session in get_db():
|
||||||
|
role = Role(name="test_role_dep")
|
||||||
|
session.add(role)
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
async with session_factory() as verify_session:
|
||||||
|
result = await RoleCrud.first(
|
||||||
|
verify_session, [Role.name == "test_role_dep"]
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateDbContext:
|
||||||
|
"""Tests for create_db_context."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_context_manager_yields_session(self):
|
||||||
|
"""Context manager yields a valid session."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
get_db_context = create_db_context(session_factory)
|
||||||
|
|
||||||
|
async with get_db_context() as session:
|
||||||
|
assert isinstance(session, AsyncSession)
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_context_manager_commits(self):
|
||||||
|
"""Context manager commits on exit."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_db_context = create_db_context(session_factory)
|
||||||
|
|
||||||
|
async with get_db_context() as session:
|
||||||
|
role = Role(name="context_role")
|
||||||
|
session.add(role)
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
async with session_factory() as verify_session:
|
||||||
|
result = await RoleCrud.first(
|
||||||
|
verify_session, [Role.name == "context_role"]
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetTransaction:
|
||||||
|
"""Tests for get_transaction context manager."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_starts_transaction(self, db_session: AsyncSession):
|
||||||
|
"""get_transaction starts a new transaction."""
|
||||||
|
async with get_transaction(db_session):
|
||||||
|
role = Role(name="tx_role")
|
||||||
|
db_session.add(role)
|
||||||
|
|
||||||
|
result = await RoleCrud.first(db_session, [Role.name == "tx_role"])
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_nested_transaction_uses_savepoint(self, db_session: AsyncSession):
|
||||||
|
"""Nested transactions use savepoints."""
|
||||||
|
async with get_transaction(db_session):
|
||||||
|
role1 = Role(name="outer_role")
|
||||||
|
db_session.add(role1)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
async with get_transaction(db_session):
|
||||||
|
role2 = Role(name="inner_role")
|
||||||
|
db_session.add(role2)
|
||||||
|
|
||||||
|
results = await RoleCrud.get_multi(db_session)
|
||||||
|
names = {r.name for r in results}
|
||||||
|
assert "outer_role" in names
|
||||||
|
assert "inner_role" in names
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_rollback_on_exception(self, db_session: AsyncSession):
|
||||||
|
"""Transaction rolls back on exception."""
|
||||||
|
try:
|
||||||
|
async with get_transaction(db_session):
|
||||||
|
role = Role(name="rollback_role")
|
||||||
|
db_session.add(role)
|
||||||
|
await db_session.flush()
|
||||||
|
raise ValueError("Simulated error")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = await RoleCrud.first(db_session, [Role.name == "rollback_role"])
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_nested_rollback_preserves_outer(self, db_session: AsyncSession):
|
||||||
|
"""Nested rollback preserves outer transaction."""
|
||||||
|
async with get_transaction(db_session):
|
||||||
|
role1 = Role(name="preserved_role")
|
||||||
|
db_session.add(role1)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with get_transaction(db_session):
|
||||||
|
role2 = Role(name="rolled_back_role")
|
||||||
|
db_session.add(role2)
|
||||||
|
await db_session.flush()
|
||||||
|
raise ValueError("Inner error")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
outer = await RoleCrud.first(db_session, [Role.name == "preserved_role"])
|
||||||
|
inner = await RoleCrud.first(db_session, [Role.name == "rolled_back_role"])
|
||||||
|
assert outer is not None
|
||||||
|
assert inner is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestLockMode:
|
||||||
|
"""Tests for LockMode enum."""
|
||||||
|
|
||||||
|
def test_lock_modes_exist(self):
|
||||||
|
"""All expected lock modes are defined."""
|
||||||
|
assert LockMode.ACCESS_SHARE == "ACCESS SHARE"
|
||||||
|
assert LockMode.ROW_SHARE == "ROW SHARE"
|
||||||
|
assert LockMode.ROW_EXCLUSIVE == "ROW EXCLUSIVE"
|
||||||
|
assert LockMode.SHARE_UPDATE_EXCLUSIVE == "SHARE UPDATE EXCLUSIVE"
|
||||||
|
assert LockMode.SHARE == "SHARE"
|
||||||
|
assert LockMode.SHARE_ROW_EXCLUSIVE == "SHARE ROW EXCLUSIVE"
|
||||||
|
assert LockMode.EXCLUSIVE == "EXCLUSIVE"
|
||||||
|
assert LockMode.ACCESS_EXCLUSIVE == "ACCESS EXCLUSIVE"
|
||||||
|
|
||||||
|
def test_lock_mode_is_string(self):
|
||||||
|
"""Lock modes are string enums."""
|
||||||
|
assert isinstance(LockMode.EXCLUSIVE, str)
|
||||||
|
assert LockMode.EXCLUSIVE.value == "EXCLUSIVE"
|
||||||
|
|
||||||
|
|
||||||
|
class TestLockTables:
|
||||||
|
"""Tests for lock_tables context manager (PostgreSQL-specific)."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_lock_single_table(self, db_session: AsyncSession):
|
||||||
|
"""Lock a single table."""
|
||||||
|
async with lock_tables(db_session, [Role]):
|
||||||
|
# Inside the lock, we can still perform operations
|
||||||
|
role = Role(name="locked_role")
|
||||||
|
db_session.add(role)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# After lock is released, verify the data was committed
|
||||||
|
result = await RoleCrud.first(db_session, [Role.name == "locked_role"])
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_lock_multiple_tables(self, db_session: AsyncSession):
|
||||||
|
"""Lock multiple tables."""
|
||||||
|
async with lock_tables(db_session, [Role, User]):
|
||||||
|
role = Role(name="multi_lock_role")
|
||||||
|
db_session.add(role)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
result = await RoleCrud.first(db_session, [Role.name == "multi_lock_role"])
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_lock_with_custom_mode(self, db_session: AsyncSession):
|
||||||
|
"""Lock with custom lock mode."""
|
||||||
|
async with lock_tables(db_session, [Role], mode=LockMode.EXCLUSIVE):
|
||||||
|
role = Role(name="exclusive_lock_role")
|
||||||
|
db_session.add(role)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
result = await RoleCrud.first(db_session, [Role.name == "exclusive_lock_role"])
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_lock_rollback_on_exception(self, db_session: AsyncSession):
|
||||||
|
"""Lock context rolls back on exception."""
|
||||||
|
try:
|
||||||
|
async with lock_tables(db_session, [Role]):
|
||||||
|
role = Role(name="lock_rollback_role")
|
||||||
|
db_session.add(role)
|
||||||
|
await db_session.flush()
|
||||||
|
raise ValueError("Simulated error")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = await RoleCrud.first(db_session, [Role.name == "lock_rollback_role"])
|
||||||
|
assert result is None
|
||||||
265
tests/test_exceptions.py
Normal file
265
tests/test_exceptions.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
"""Tests for fastapi_toolsets.exceptions module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from fastapi_toolsets.exceptions import (
|
||||||
|
ApiException,
|
||||||
|
ConflictError,
|
||||||
|
ForbiddenError,
|
||||||
|
NotFoundError,
|
||||||
|
UnauthorizedError,
|
||||||
|
generate_error_responses,
|
||||||
|
init_exceptions_handlers,
|
||||||
|
)
|
||||||
|
from fastapi_toolsets.schemas import ApiError
|
||||||
|
|
||||||
|
|
||||||
|
class TestApiException:
|
||||||
|
"""Tests for ApiException base class."""
|
||||||
|
|
||||||
|
def test_subclass_with_api_error(self):
|
||||||
|
"""Subclasses can define api_error."""
|
||||||
|
|
||||||
|
class CustomError(ApiException):
|
||||||
|
api_error = ApiError(
|
||||||
|
code=418,
|
||||||
|
msg="I'm a teapot",
|
||||||
|
desc="The server is a teapot.",
|
||||||
|
err_code="TEA-418",
|
||||||
|
)
|
||||||
|
|
||||||
|
error = CustomError()
|
||||||
|
assert error.api_error.code == 418
|
||||||
|
assert error.api_error.msg == "I'm a teapot"
|
||||||
|
assert str(error) == "I'm a teapot"
|
||||||
|
|
||||||
|
def test_custom_detail_message(self):
|
||||||
|
"""Custom detail overrides default message."""
|
||||||
|
|
||||||
|
class CustomError(ApiException):
|
||||||
|
api_error = ApiError(
|
||||||
|
code=400,
|
||||||
|
msg="Bad Request",
|
||||||
|
desc="Request was bad.",
|
||||||
|
err_code="BAD-400",
|
||||||
|
)
|
||||||
|
|
||||||
|
error = CustomError("Custom message")
|
||||||
|
assert str(error) == "Custom message"
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuiltInExceptions:
|
||||||
|
"""Tests for built-in exception classes."""
|
||||||
|
|
||||||
|
def test_unauthorized_error(self):
|
||||||
|
"""UnauthorizedError has correct attributes."""
|
||||||
|
error = UnauthorizedError()
|
||||||
|
assert error.api_error.code == 401
|
||||||
|
assert error.api_error.err_code == "AUTH-401"
|
||||||
|
|
||||||
|
def test_forbidden_error(self):
|
||||||
|
"""ForbiddenError has correct attributes."""
|
||||||
|
error = ForbiddenError()
|
||||||
|
assert error.api_error.code == 403
|
||||||
|
assert error.api_error.err_code == "AUTH-403"
|
||||||
|
|
||||||
|
def test_not_found_error(self):
|
||||||
|
"""NotFoundError has correct attributes."""
|
||||||
|
error = NotFoundError()
|
||||||
|
assert error.api_error.code == 404
|
||||||
|
assert error.api_error.err_code == "RES-404"
|
||||||
|
|
||||||
|
def test_conflict_error(self):
|
||||||
|
"""ConflictError has correct attributes."""
|
||||||
|
error = ConflictError()
|
||||||
|
assert error.api_error.code == 409
|
||||||
|
assert error.api_error.err_code == "RES-409"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateErrorResponses:
|
||||||
|
"""Tests for generate_error_responses function."""
|
||||||
|
|
||||||
|
def test_generates_single_response(self):
|
||||||
|
"""Generates response for single exception."""
|
||||||
|
responses = generate_error_responses(NotFoundError)
|
||||||
|
|
||||||
|
assert 404 in responses
|
||||||
|
assert responses[404]["description"] == "Not Found"
|
||||||
|
|
||||||
|
def test_generates_multiple_responses(self):
|
||||||
|
"""Generates responses for multiple exceptions."""
|
||||||
|
responses = generate_error_responses(
|
||||||
|
UnauthorizedError,
|
||||||
|
ForbiddenError,
|
||||||
|
NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 401 in responses
|
||||||
|
assert 403 in responses
|
||||||
|
assert 404 in responses
|
||||||
|
|
||||||
|
def test_response_has_example(self):
|
||||||
|
"""Generated response includes example."""
|
||||||
|
responses = generate_error_responses(NotFoundError)
|
||||||
|
example = responses[404]["content"]["application/json"]["example"]
|
||||||
|
|
||||||
|
assert example["status"] == "FAIL"
|
||||||
|
assert example["error_code"] == "RES-404"
|
||||||
|
assert example["message"] == "Not Found"
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitExceptionsHandlers:
|
||||||
|
"""Tests for init_exceptions_handlers function."""
|
||||||
|
|
||||||
|
def test_returns_app(self):
|
||||||
|
"""Returns the FastAPI app."""
|
||||||
|
app = FastAPI()
|
||||||
|
result = init_exceptions_handlers(app)
|
||||||
|
assert result is app
|
||||||
|
|
||||||
|
def test_handles_api_exception(self):
|
||||||
|
"""Handles ApiException with structured response."""
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/error")
|
||||||
|
async def raise_error():
|
||||||
|
raise NotFoundError()
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/error")
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "FAIL"
|
||||||
|
assert data["error_code"] == "RES-404"
|
||||||
|
assert data["message"] == "Not Found"
|
||||||
|
|
||||||
|
def test_handles_validation_error(self):
|
||||||
|
"""Handles validation errors with structured response."""
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
|
||||||
|
class Item(BaseModel):
|
||||||
|
name: str
|
||||||
|
price: float
|
||||||
|
|
||||||
|
@app.post("/items")
|
||||||
|
async def create_item(item: Item):
|
||||||
|
return item
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.post("/items", json={"name": 123})
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "FAIL"
|
||||||
|
assert data["error_code"] == "VAL-422"
|
||||||
|
assert "errors" in data["data"]
|
||||||
|
|
||||||
|
def test_handles_generic_exception(self):
|
||||||
|
"""Handles unhandled exceptions with 500 response."""
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/crash")
|
||||||
|
async def crash():
|
||||||
|
raise RuntimeError("Something went wrong")
|
||||||
|
|
||||||
|
client = TestClient(app, raise_server_exceptions=False)
|
||||||
|
response = client.get("/crash")
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "FAIL"
|
||||||
|
assert data["error_code"] == "SERVER-500"
|
||||||
|
|
||||||
|
def test_custom_openapi_schema(self):
|
||||||
|
"""Customizes OpenAPI schema for 422 responses."""
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Item(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
@app.post("/items")
|
||||||
|
async def create_item(item: Item):
|
||||||
|
return item
|
||||||
|
|
||||||
|
openapi = app.openapi()
|
||||||
|
|
||||||
|
post_op = openapi["paths"]["/items"]["post"]
|
||||||
|
assert "422" in post_op["responses"]
|
||||||
|
resp_422 = post_op["responses"]["422"]
|
||||||
|
example = resp_422["content"]["application/json"]["example"]
|
||||||
|
assert example["error_code"] == "VAL-422"
|
||||||
|
|
||||||
|
|
||||||
|
class TestExceptionIntegration:
|
||||||
|
"""Integration tests for exception handling."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_with_routes(self):
|
||||||
|
"""Create app with test routes."""
|
||||||
|
app = FastAPI()
|
||||||
|
init_exceptions_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/users/{user_id}")
|
||||||
|
async def get_user(user_id: int):
|
||||||
|
if user_id == 404:
|
||||||
|
raise NotFoundError()
|
||||||
|
if user_id == 401:
|
||||||
|
raise UnauthorizedError()
|
||||||
|
if user_id == 403:
|
||||||
|
raise ForbiddenError()
|
||||||
|
if user_id == 409:
|
||||||
|
raise ConflictError()
|
||||||
|
return {"id": user_id}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
def test_not_found_response(self, app_with_routes):
|
||||||
|
"""NotFoundError returns 404."""
|
||||||
|
client = TestClient(app_with_routes)
|
||||||
|
response = client.get("/users/404")
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json()["error_code"] == "RES-404"
|
||||||
|
|
||||||
|
def test_unauthorized_response(self, app_with_routes):
|
||||||
|
"""UnauthorizedError returns 401."""
|
||||||
|
client = TestClient(app_with_routes)
|
||||||
|
response = client.get("/users/401")
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json()["error_code"] == "AUTH-401"
|
||||||
|
|
||||||
|
def test_forbidden_response(self, app_with_routes):
|
||||||
|
"""ForbiddenError returns 403."""
|
||||||
|
client = TestClient(app_with_routes)
|
||||||
|
response = client.get("/users/403")
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json()["error_code"] == "AUTH-403"
|
||||||
|
|
||||||
|
def test_conflict_response(self, app_with_routes):
|
||||||
|
"""ConflictError returns 409."""
|
||||||
|
client = TestClient(app_with_routes)
|
||||||
|
response = client.get("/users/409")
|
||||||
|
|
||||||
|
assert response.status_code == 409
|
||||||
|
assert response.json()["error_code"] == "RES-409"
|
||||||
|
|
||||||
|
def test_success_response(self, app_with_routes):
|
||||||
|
"""Successful requests return normally."""
|
||||||
|
client = TestClient(app_with_routes)
|
||||||
|
response = client.get("/users/1")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"id": 1}
|
||||||
401
tests/test_fixtures.py
Normal file
401
tests/test_fixtures.py
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
"""Tests for fastapi_toolsets.fixtures module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from fastapi_toolsets.fixtures import (
|
||||||
|
Context,
|
||||||
|
FixtureRegistry,
|
||||||
|
LoadStrategy,
|
||||||
|
load_fixtures,
|
||||||
|
load_fixtures_by_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .conftest import Role, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestContext:
|
||||||
|
"""Tests for Context enum."""
|
||||||
|
|
||||||
|
def test_base_context(self):
|
||||||
|
"""BASE context has correct value."""
|
||||||
|
assert Context.BASE.value == "base"
|
||||||
|
|
||||||
|
def test_production_context(self):
|
||||||
|
"""PRODUCTION context has correct value."""
|
||||||
|
assert Context.PRODUCTION.value == "production"
|
||||||
|
|
||||||
|
def test_development_context(self):
|
||||||
|
"""DEVELOPMENT context has correct value."""
|
||||||
|
assert Context.DEVELOPMENT.value == "development"
|
||||||
|
|
||||||
|
def test_testing_context(self):
|
||||||
|
"""TESTING context has correct value."""
|
||||||
|
assert Context.TESTING.value == "testing"
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoadStrategy:
|
||||||
|
"""Tests for LoadStrategy enum."""
|
||||||
|
|
||||||
|
def test_insert_strategy(self):
|
||||||
|
"""INSERT strategy has correct value."""
|
||||||
|
assert LoadStrategy.INSERT.value == "insert"
|
||||||
|
|
||||||
|
def test_merge_strategy(self):
|
||||||
|
"""MERGE strategy has correct value."""
|
||||||
|
assert LoadStrategy.MERGE.value == "merge"
|
||||||
|
|
||||||
|
def test_skip_existing_strategy(self):
|
||||||
|
"""SKIP_EXISTING strategy has correct value."""
|
||||||
|
assert LoadStrategy.SKIP_EXISTING.value == "skip_existing"
|
||||||
|
|
||||||
|
|
||||||
|
class TestFixtureRegistry:
|
||||||
|
"""Tests for FixtureRegistry class."""
|
||||||
|
|
||||||
|
def test_register_with_decorator(self):
|
||||||
|
"""Register fixture with decorator."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
assert "roles" in [f.name for f in registry.get_all()]
|
||||||
|
|
||||||
|
def test_register_with_custom_name(self):
|
||||||
|
"""Register fixture with custom name."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(name="custom_roles")
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
fixture = registry.get("custom_roles")
|
||||||
|
assert fixture.name == "custom_roles"
|
||||||
|
|
||||||
|
def test_register_with_dependencies(self):
|
||||||
|
"""Register fixture with dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
@registry.register(depends_on=["roles"])
|
||||||
|
def users():
|
||||||
|
return [User(id=1, username="admin", email="admin@test.com", role_id=1)]
|
||||||
|
|
||||||
|
fixture = registry.get("users")
|
||||||
|
assert fixture.depends_on == ["roles"]
|
||||||
|
|
||||||
|
def test_register_with_contexts(self):
|
||||||
|
"""Register fixture with contexts."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.TESTING])
|
||||||
|
def test_data():
|
||||||
|
return [Role(id=100, name="test")]
|
||||||
|
|
||||||
|
fixture = registry.get("test_data")
|
||||||
|
assert Context.TESTING.value in fixture.contexts
|
||||||
|
|
||||||
|
def test_get_raises_key_error(self):
|
||||||
|
"""Get raises KeyError for missing fixture."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
with pytest.raises(KeyError, match="not found"):
|
||||||
|
registry.get("nonexistent")
|
||||||
|
|
||||||
|
def test_get_all(self):
|
||||||
|
"""Get all registered fixtures."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def fixture1():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def fixture2():
|
||||||
|
return []
|
||||||
|
|
||||||
|
fixtures = registry.get_all()
|
||||||
|
names = {f.name for f in fixtures}
|
||||||
|
assert names == {"fixture1", "fixture2"}
|
||||||
|
|
||||||
|
def test_get_by_context(self):
|
||||||
|
"""Get fixtures by context."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.BASE])
|
||||||
|
def base_data():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.TESTING])
|
||||||
|
def test_data():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.PRODUCTION])
|
||||||
|
def prod_data():
|
||||||
|
return []
|
||||||
|
|
||||||
|
testing_fixtures = registry.get_by_context(Context.TESTING)
|
||||||
|
names = {f.name for f in testing_fixtures}
|
||||||
|
assert names == {"test_data"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestDependencyResolution:
|
||||||
|
"""Tests for fixture dependency resolution."""
|
||||||
|
|
||||||
|
def test_resolve_simple_dependency(self):
|
||||||
|
"""Resolve simple dependency chain."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(depends_on=["roles"])
|
||||||
|
def users():
|
||||||
|
return []
|
||||||
|
|
||||||
|
order = registry.resolve_dependencies("users")
|
||||||
|
assert order == ["roles", "users"]
|
||||||
|
|
||||||
|
def test_resolve_multiple_dependencies(self):
|
||||||
|
"""Resolve multiple dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def permissions():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(depends_on=["roles", "permissions"])
|
||||||
|
def users():
|
||||||
|
return []
|
||||||
|
|
||||||
|
order = registry.resolve_dependencies("users")
|
||||||
|
assert "roles" in order
|
||||||
|
assert "permissions" in order
|
||||||
|
assert order.index("roles") < order.index("users")
|
||||||
|
assert order.index("permissions") < order.index("users")
|
||||||
|
|
||||||
|
def test_resolve_transitive_dependencies(self):
|
||||||
|
"""Resolve transitive dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def base():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(depends_on=["base"])
|
||||||
|
def middle():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(depends_on=["middle"])
|
||||||
|
def top():
|
||||||
|
return []
|
||||||
|
|
||||||
|
order = registry.resolve_dependencies("top")
|
||||||
|
assert order == ["base", "middle", "top"]
|
||||||
|
|
||||||
|
def test_detect_circular_dependency(self):
|
||||||
|
"""Detect circular dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(depends_on=["b"])
|
||||||
|
def a():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(depends_on=["a"])
|
||||||
|
def b():
|
||||||
|
return []
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Circular dependency"):
|
||||||
|
registry.resolve_dependencies("a")
|
||||||
|
|
||||||
|
def test_resolve_context_dependencies(self):
|
||||||
|
"""Resolve all fixtures for a context with dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.BASE])
|
||||||
|
def roles():
|
||||||
|
return []
|
||||||
|
|
||||||
|
@registry.register(depends_on=["roles"], contexts=[Context.TESTING])
|
||||||
|
def test_users():
|
||||||
|
return []
|
||||||
|
|
||||||
|
order = registry.resolve_context_dependencies(Context.BASE, Context.TESTING)
|
||||||
|
assert "roles" in order
|
||||||
|
assert "test_users" in order
|
||||||
|
assert order.index("roles") < order.index("test_users")
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoadFixtures:
|
||||||
|
"""Tests for load_fixtures function."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_single_fixture(self, db_session: AsyncSession):
|
||||||
|
"""Load a single fixture."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return [
|
||||||
|
Role(id=1, name="admin"),
|
||||||
|
Role(id=2, name="user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = await load_fixtures(db_session, registry, "roles")
|
||||||
|
|
||||||
|
assert "roles" in result
|
||||||
|
assert len(result["roles"]) == 2
|
||||||
|
|
||||||
|
from .conftest import RoleCrud
|
||||||
|
|
||||||
|
count = await RoleCrud.count(db_session)
|
||||||
|
assert count == 2
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_with_dependencies(self, db_session: AsyncSession):
|
||||||
|
"""Load fixtures with dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
@registry.register(depends_on=["roles"])
|
||||||
|
def users():
|
||||||
|
return [User(id=1, username="admin", email="admin@test.com", role_id=1)]
|
||||||
|
|
||||||
|
result = await load_fixtures(db_session, registry, "users")
|
||||||
|
|
||||||
|
assert "roles" in result
|
||||||
|
assert "users" in result
|
||||||
|
|
||||||
|
from .conftest import RoleCrud, UserCrud
|
||||||
|
|
||||||
|
assert await RoleCrud.count(db_session) == 1
|
||||||
|
assert await UserCrud.count(db_session) == 1
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_with_merge_strategy(self, db_session: AsyncSession):
|
||||||
|
"""Load fixtures with MERGE strategy updates existing."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
await load_fixtures(db_session, registry, "roles", strategy=LoadStrategy.MERGE)
|
||||||
|
await load_fixtures(db_session, registry, "roles", strategy=LoadStrategy.MERGE)
|
||||||
|
|
||||||
|
from .conftest import RoleCrud
|
||||||
|
|
||||||
|
count = await RoleCrud.count(db_session)
|
||||||
|
assert count == 1
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_with_skip_existing_strategy(self, db_session: AsyncSession):
|
||||||
|
"""Load fixtures with SKIP_EXISTING strategy."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="original")]
|
||||||
|
|
||||||
|
await load_fixtures(
|
||||||
|
db_session, registry, "roles", strategy=LoadStrategy.SKIP_EXISTING
|
||||||
|
)
|
||||||
|
|
||||||
|
@registry.register(name="roles_updated")
|
||||||
|
def roles_v2():
|
||||||
|
return [Role(id=1, name="updated")]
|
||||||
|
|
||||||
|
registry._fixtures["roles"] = registry._fixtures.pop("roles_updated")
|
||||||
|
|
||||||
|
await load_fixtures(
|
||||||
|
db_session, registry, "roles", strategy=LoadStrategy.SKIP_EXISTING
|
||||||
|
)
|
||||||
|
|
||||||
|
from .conftest import RoleCrud
|
||||||
|
|
||||||
|
role = await RoleCrud.first(db_session, [Role.id == 1])
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "original"
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoadFixturesByContext:
|
||||||
|
"""Tests for load_fixtures_by_context function."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_by_single_context(self, db_session: AsyncSession):
|
||||||
|
"""Load fixtures by single context."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.BASE])
|
||||||
|
def base_roles():
|
||||||
|
return [Role(id=1, name="base_role")]
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.TESTING])
|
||||||
|
def test_roles():
|
||||||
|
return [Role(id=100, name="test_role")]
|
||||||
|
|
||||||
|
await load_fixtures_by_context(db_session, registry, Context.BASE)
|
||||||
|
|
||||||
|
from .conftest import RoleCrud
|
||||||
|
|
||||||
|
count = await RoleCrud.count(db_session)
|
||||||
|
assert count == 1
|
||||||
|
|
||||||
|
role = await RoleCrud.first(db_session, [Role.id == 1])
|
||||||
|
assert role is not None
|
||||||
|
assert role.name == "base_role"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_by_multiple_contexts(self, db_session: AsyncSession):
|
||||||
|
"""Load fixtures by multiple contexts."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.BASE])
|
||||||
|
def base_roles():
|
||||||
|
return [Role(id=1, name="base_role")]
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.TESTING])
|
||||||
|
def test_roles():
|
||||||
|
return [Role(id=100, name="test_role")]
|
||||||
|
|
||||||
|
await load_fixtures_by_context(
|
||||||
|
db_session, registry, Context.BASE, Context.TESTING
|
||||||
|
)
|
||||||
|
|
||||||
|
from .conftest import RoleCrud
|
||||||
|
|
||||||
|
count = await RoleCrud.count(db_session)
|
||||||
|
assert count == 2
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_context_with_dependencies(self, db_session: AsyncSession):
|
||||||
|
"""Load context fixtures with cross-context dependencies."""
|
||||||
|
registry = FixtureRegistry()
|
||||||
|
|
||||||
|
@registry.register(contexts=[Context.BASE])
|
||||||
|
def roles():
|
||||||
|
return [Role(id=1, name="admin")]
|
||||||
|
|
||||||
|
@registry.register(depends_on=["roles"], contexts=[Context.TESTING])
|
||||||
|
def test_users():
|
||||||
|
return [User(id=1, username="tester", email="test@test.com", role_id=1)]
|
||||||
|
|
||||||
|
await load_fixtures_by_context(db_session, registry, Context.TESTING)
|
||||||
|
|
||||||
|
from .conftest import RoleCrud, UserCrud
|
||||||
|
|
||||||
|
assert await RoleCrud.count(db_session) == 1
|
||||||
|
assert await UserCrud.count(db_session) == 1
|
||||||
160
tests/test_pytest_plugin.py
Normal file
160
tests/test_pytest_plugin.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""Tests for fastapi_toolsets.pytest_plugin module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from fastapi_toolsets.fixtures import Context, FixtureRegistry, register_fixtures
|
||||||
|
|
||||||
|
from .conftest import Role, RoleCrud, User, UserCrud
|
||||||
|
|
||||||
|
test_registry = FixtureRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
@test_registry.register(contexts=[Context.BASE])
|
||||||
|
def roles() -> list[Role]:
|
||||||
|
return [
|
||||||
|
Role(id=1000, name="plugin_admin"),
|
||||||
|
Role(id=1001, name="plugin_user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@test_registry.register(depends_on=["roles"], contexts=[Context.BASE])
|
||||||
|
def users() -> list[User]:
|
||||||
|
return [
|
||||||
|
User(id=1000, username="plugin_admin", email="padmin@test.com", role_id=1000),
|
||||||
|
User(id=1001, username="plugin_user", email="puser@test.com", role_id=1001),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@test_registry.register(depends_on=["users"], contexts=[Context.TESTING])
|
||||||
|
def extra_users() -> list[User]:
|
||||||
|
return [
|
||||||
|
User(id=1002, username="plugin_extra", email="pextra@test.com", role_id=1001),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
register_fixtures(test_registry, globals())
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegisterFixtures:
|
||||||
|
"""Tests for register_fixtures function."""
|
||||||
|
|
||||||
|
def test_creates_fixtures_in_namespace(self):
|
||||||
|
"""Fixtures are created in the namespace."""
|
||||||
|
assert "fixture_roles" in globals()
|
||||||
|
assert "fixture_users" in globals()
|
||||||
|
assert "fixture_extra_users" in globals()
|
||||||
|
|
||||||
|
def test_fixtures_are_callable(self):
|
||||||
|
"""Created fixtures are callable."""
|
||||||
|
assert callable(globals()["fixture_roles"])
|
||||||
|
assert callable(globals()["fixture_users"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestGeneratedFixtures:
|
||||||
|
"""Tests for the generated pytest fixtures."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_fixture_loads_data(
|
||||||
|
self, db_session: AsyncSession, fixture_roles: list[Role]
|
||||||
|
):
|
||||||
|
"""Fixture loads data into database and returns it."""
|
||||||
|
assert len(fixture_roles) == 2
|
||||||
|
assert fixture_roles[0].name == "plugin_admin"
|
||||||
|
assert fixture_roles[1].name == "plugin_user"
|
||||||
|
|
||||||
|
# Verify data is in database
|
||||||
|
count = await RoleCrud.count(db_session, [Role.id >= 1000])
|
||||||
|
assert count == 2
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_fixture_with_dependency(
|
||||||
|
self, db_session: AsyncSession, fixture_users: list[User]
|
||||||
|
):
|
||||||
|
"""Fixture with dependency loads parent fixture first."""
|
||||||
|
# fixture_users depends on fixture_roles
|
||||||
|
# Both should be loaded
|
||||||
|
assert len(fixture_users) == 2
|
||||||
|
|
||||||
|
# Roles should also be in database
|
||||||
|
roles_count = await RoleCrud.count(db_session, [Role.id >= 1000])
|
||||||
|
assert roles_count == 2
|
||||||
|
|
||||||
|
# Users should be in database
|
||||||
|
users_count = await UserCrud.count(db_session, [User.id >= 1000])
|
||||||
|
assert users_count == 2
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_fixture_returns_models(
|
||||||
|
self, db_session: AsyncSession, fixture_users: list[User]
|
||||||
|
):
|
||||||
|
"""Fixture returns actual model instances."""
|
||||||
|
user = fixture_users[0]
|
||||||
|
assert isinstance(user, User)
|
||||||
|
assert user.id == 1000
|
||||||
|
assert user.username == "plugin_admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_fixture_relationships_work(
|
||||||
|
self, db_session: AsyncSession, fixture_users: list[User]
|
||||||
|
):
|
||||||
|
"""Loaded fixtures have working relationships."""
|
||||||
|
# Load user with role relationship
|
||||||
|
user = await UserCrud.get(
|
||||||
|
db_session,
|
||||||
|
[User.id == 1000],
|
||||||
|
load_options=[selectinload(User.role)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user.role is not None
|
||||||
|
assert user.role.name == "plugin_admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_chained_dependencies(
|
||||||
|
self, db_session: AsyncSession, fixture_extra_users: list[User]
|
||||||
|
):
|
||||||
|
"""Chained dependencies are resolved correctly."""
|
||||||
|
# fixture_extra_users -> fixture_users -> fixture_roles
|
||||||
|
assert len(fixture_extra_users) == 1
|
||||||
|
|
||||||
|
# All fixtures should be loaded
|
||||||
|
roles_count = await RoleCrud.count(db_session, [Role.id >= 1000])
|
||||||
|
users_count = await UserCrud.count(db_session, [User.id >= 1000])
|
||||||
|
|
||||||
|
assert roles_count == 2
|
||||||
|
assert users_count == 3 # 2 from users + 1 from extra_users
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_can_query_loaded_data(
|
||||||
|
self, db_session: AsyncSession, fixture_users: list[User]
|
||||||
|
):
|
||||||
|
"""Can query the loaded fixture data."""
|
||||||
|
# Get all users loaded by fixture
|
||||||
|
users = await UserCrud.get_multi(
|
||||||
|
db_session,
|
||||||
|
filters=[User.id >= 1000],
|
||||||
|
order_by=User.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(users) == 2
|
||||||
|
assert users[0].username == "plugin_admin"
|
||||||
|
assert users[1].username == "plugin_user"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_multiple_fixtures_in_same_test(
|
||||||
|
self,
|
||||||
|
db_session: AsyncSession,
|
||||||
|
fixture_roles: list[Role],
|
||||||
|
fixture_users: list[User],
|
||||||
|
):
|
||||||
|
"""Multiple fixtures can be used in the same test."""
|
||||||
|
assert len(fixture_roles) == 2
|
||||||
|
assert len(fixture_users) == 2
|
||||||
|
|
||||||
|
# Both should be in database
|
||||||
|
roles = await RoleCrud.get_multi(db_session, filters=[Role.id >= 1000])
|
||||||
|
users = await UserCrud.get_multi(db_session, filters=[User.id >= 1000])
|
||||||
|
|
||||||
|
assert len(roles) == 2
|
||||||
|
assert len(users) == 2
|
||||||
284
tests/test_schemas.py
Normal file
284
tests/test_schemas.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
"""Tests for fastapi_toolsets.schemas module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from fastapi_toolsets.schemas import (
|
||||||
|
ApiError,
|
||||||
|
ErrorResponse,
|
||||||
|
PaginatedResponse,
|
||||||
|
Pagination,
|
||||||
|
Response,
|
||||||
|
ResponseStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponseStatus:
|
||||||
|
"""Tests for ResponseStatus enum."""
|
||||||
|
|
||||||
|
def test_success_value(self):
|
||||||
|
"""SUCCESS has correct value."""
|
||||||
|
assert ResponseStatus.SUCCESS.value == "SUCCESS"
|
||||||
|
|
||||||
|
def test_fail_value(self):
|
||||||
|
"""FAIL has correct value."""
|
||||||
|
assert ResponseStatus.FAIL.value == "FAIL"
|
||||||
|
|
||||||
|
def test_is_string_enum(self):
|
||||||
|
"""ResponseStatus is a string enum."""
|
||||||
|
assert isinstance(ResponseStatus.SUCCESS, str)
|
||||||
|
|
||||||
|
|
||||||
|
class TestApiError:
|
||||||
|
"""Tests for ApiError schema."""
|
||||||
|
|
||||||
|
def test_create_api_error(self):
|
||||||
|
"""Create ApiError with all fields."""
|
||||||
|
error = ApiError(
|
||||||
|
code=404,
|
||||||
|
msg="Not Found",
|
||||||
|
desc="The resource was not found.",
|
||||||
|
err_code="RES-404",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert error.code == 404
|
||||||
|
assert error.msg == "Not Found"
|
||||||
|
assert error.desc == "The resource was not found."
|
||||||
|
assert error.err_code == "RES-404"
|
||||||
|
|
||||||
|
def test_requires_all_fields(self):
|
||||||
|
"""ApiError requires all fields."""
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
ApiError(code=404, msg="Not Found") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponse:
|
||||||
|
"""Tests for Response schema."""
|
||||||
|
|
||||||
|
def test_create_with_data(self):
|
||||||
|
"""Create Response with data."""
|
||||||
|
response = Response(data={"id": 1, "name": "test"})
|
||||||
|
|
||||||
|
assert response.data == {"id": 1, "name": "test"}
|
||||||
|
assert response.status == ResponseStatus.SUCCESS
|
||||||
|
assert response.message == "Success"
|
||||||
|
assert response.error_code is None
|
||||||
|
|
||||||
|
def test_create_with_custom_message(self):
|
||||||
|
"""Create Response with custom message."""
|
||||||
|
response = Response(data="result", message="Operation completed")
|
||||||
|
|
||||||
|
assert response.message == "Operation completed"
|
||||||
|
|
||||||
|
def test_create_with_none_data(self):
|
||||||
|
"""Create Response with None data."""
|
||||||
|
response = Response[dict](data=None)
|
||||||
|
|
||||||
|
assert response.data is None
|
||||||
|
assert response.status == ResponseStatus.SUCCESS
|
||||||
|
|
||||||
|
def test_generic_type_hint(self):
|
||||||
|
"""Response supports generic type hints."""
|
||||||
|
response: Response[list[str]] = Response(data=["a", "b", "c"])
|
||||||
|
|
||||||
|
assert response.data == ["a", "b", "c"]
|
||||||
|
|
||||||
|
def test_serialization(self):
|
||||||
|
"""Response serializes correctly."""
|
||||||
|
response = Response(data={"key": "value"}, message="Test")
|
||||||
|
data = response.model_dump()
|
||||||
|
|
||||||
|
assert data["status"] == "SUCCESS"
|
||||||
|
assert data["message"] == "Test"
|
||||||
|
assert data["data"] == {"key": "value"}
|
||||||
|
assert data["error_code"] is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrorResponse:
|
||||||
|
"""Tests for ErrorResponse schema."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""ErrorResponse has correct defaults."""
|
||||||
|
response = ErrorResponse()
|
||||||
|
|
||||||
|
assert response.status == ResponseStatus.FAIL
|
||||||
|
assert response.data is None
|
||||||
|
|
||||||
|
def test_with_description(self):
|
||||||
|
"""ErrorResponse with description."""
|
||||||
|
response = ErrorResponse(
|
||||||
|
message="Bad Request",
|
||||||
|
description="The request was invalid.",
|
||||||
|
error_code="BAD-400",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.message == "Bad Request"
|
||||||
|
assert response.description == "The request was invalid."
|
||||||
|
assert response.error_code == "BAD-400"
|
||||||
|
|
||||||
|
def test_serialization(self):
|
||||||
|
"""ErrorResponse serializes correctly."""
|
||||||
|
response = ErrorResponse(
|
||||||
|
message="Error",
|
||||||
|
description="Details",
|
||||||
|
error_code="ERR-500",
|
||||||
|
)
|
||||||
|
data = response.model_dump()
|
||||||
|
|
||||||
|
assert data["status"] == "FAIL"
|
||||||
|
assert data["description"] == "Details"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPagination:
|
||||||
|
"""Tests for Pagination schema."""
|
||||||
|
|
||||||
|
def test_create_pagination(self):
|
||||||
|
"""Create Pagination with all fields."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=100,
|
||||||
|
items_per_page=10,
|
||||||
|
page=1,
|
||||||
|
has_more=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert pagination.total_count == 100
|
||||||
|
assert pagination.items_per_page == 10
|
||||||
|
assert pagination.page == 1
|
||||||
|
assert pagination.has_more is True
|
||||||
|
|
||||||
|
def test_last_page_has_more_false(self):
|
||||||
|
"""Last page has has_more=False."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=25,
|
||||||
|
items_per_page=10,
|
||||||
|
page=3,
|
||||||
|
has_more=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert pagination.has_more is False
|
||||||
|
|
||||||
|
def test_serialization(self):
|
||||||
|
"""Pagination serializes correctly."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=50,
|
||||||
|
items_per_page=20,
|
||||||
|
page=2,
|
||||||
|
has_more=True,
|
||||||
|
)
|
||||||
|
data = pagination.model_dump()
|
||||||
|
|
||||||
|
assert data["total_count"] == 50
|
||||||
|
assert data["items_per_page"] == 20
|
||||||
|
assert data["page"] == 2
|
||||||
|
assert data["has_more"] is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestPaginatedResponse:
|
||||||
|
"""Tests for PaginatedResponse schema."""
|
||||||
|
|
||||||
|
def test_create_paginated_response(self):
|
||||||
|
"""Create PaginatedResponse with data and pagination."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=30,
|
||||||
|
items_per_page=10,
|
||||||
|
page=1,
|
||||||
|
has_more=True,
|
||||||
|
)
|
||||||
|
response = PaginatedResponse(
|
||||||
|
data=[{"id": 1}, {"id": 2}],
|
||||||
|
pagination=pagination,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(response.data) == 2
|
||||||
|
assert response.pagination.total_count == 30
|
||||||
|
assert response.status == ResponseStatus.SUCCESS
|
||||||
|
|
||||||
|
def test_with_custom_message(self):
|
||||||
|
"""PaginatedResponse with custom message."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=5,
|
||||||
|
items_per_page=10,
|
||||||
|
page=1,
|
||||||
|
has_more=False,
|
||||||
|
)
|
||||||
|
response = PaginatedResponse(
|
||||||
|
data=[1, 2, 3, 4, 5],
|
||||||
|
pagination=pagination,
|
||||||
|
message="Found 5 items",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.message == "Found 5 items"
|
||||||
|
|
||||||
|
def test_empty_data(self):
|
||||||
|
"""PaginatedResponse with empty data."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=0,
|
||||||
|
items_per_page=10,
|
||||||
|
page=1,
|
||||||
|
has_more=False,
|
||||||
|
)
|
||||||
|
response = PaginatedResponse[dict](
|
||||||
|
data=[],
|
||||||
|
pagination=pagination,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.data == []
|
||||||
|
assert response.pagination.total_count == 0
|
||||||
|
|
||||||
|
def test_generic_type_hint(self):
|
||||||
|
"""PaginatedResponse supports generic type hints."""
|
||||||
|
|
||||||
|
class UserOut:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=1,
|
||||||
|
items_per_page=10,
|
||||||
|
page=1,
|
||||||
|
has_more=False,
|
||||||
|
)
|
||||||
|
response: PaginatedResponse[dict] = PaginatedResponse(
|
||||||
|
data=[{"id": 1, "name": "test"}],
|
||||||
|
pagination=pagination,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.data[0]["id"] == 1
|
||||||
|
|
||||||
|
def test_serialization(self):
|
||||||
|
"""PaginatedResponse serializes correctly."""
|
||||||
|
pagination = Pagination(
|
||||||
|
total_count=100,
|
||||||
|
items_per_page=10,
|
||||||
|
page=5,
|
||||||
|
has_more=True,
|
||||||
|
)
|
||||||
|
response = PaginatedResponse(
|
||||||
|
data=["item1", "item2"],
|
||||||
|
pagination=pagination,
|
||||||
|
message="Page 5",
|
||||||
|
)
|
||||||
|
data = response.model_dump()
|
||||||
|
|
||||||
|
assert data["status"] == "SUCCESS"
|
||||||
|
assert data["message"] == "Page 5"
|
||||||
|
assert data["data"] == ["item1", "item2"]
|
||||||
|
assert data["pagination"]["page"] == 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestFromAttributes:
|
||||||
|
"""Tests for from_attributes config (ORM mode)."""
|
||||||
|
|
||||||
|
def test_response_from_orm_object(self):
|
||||||
|
"""Response can accept ORM-like objects."""
|
||||||
|
|
||||||
|
class FakeOrmObject:
|
||||||
|
def __init__(self):
|
||||||
|
self.id = 1
|
||||||
|
self.name = "test"
|
||||||
|
|
||||||
|
obj = FakeOrmObject()
|
||||||
|
response = Response(data=obj)
|
||||||
|
|
||||||
|
assert response.data.id == 1 # type: ignore
|
||||||
|
assert response.data.name == "test" # type: ignore
|
||||||
793
uv.lock
generated
Normal file
793
uv.lock
generated
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-doc"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asyncpg"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.1.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "coverage"
|
||||||
|
version = "7.13.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
toml = [
|
||||||
|
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi"
|
||||||
|
version = "0.128.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi-toolsets"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asyncpg" },
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||||
|
{ name = "typer" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-anyio" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
{ name = "ty" },
|
||||||
|
]
|
||||||
|
test = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-anyio" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||||
|
{ name = "coverage", marker = "extra == 'test'", specifier = ">=7.0.0" },
|
||||||
|
{ name = "fastapi", specifier = ">=0.100.0" },
|
||||||
|
{ name = "fastapi-toolsets", extras = ["test"], marker = "extra == 'dev'" },
|
||||||
|
{ name = "httpx", specifier = ">=0.25.0" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.0" },
|
||||||
|
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
|
||||||
|
{ name = "pytest-anyio", marker = "extra == 'test'", specifier = ">=0.0.0" },
|
||||||
|
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0.0" },
|
||||||
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
||||||
|
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0" },
|
||||||
|
{ name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a0" },
|
||||||
|
{ name = "typer", specifier = ">=0.9.0" },
|
||||||
|
]
|
||||||
|
provides-extras = ["test", "dev"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.12.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.41.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-anyio"
|
||||||
|
version = "0.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/00/44/a02e5877a671b0940f21a7a0d9704c22097b123ed5cdbcca9cab39f17acc/pytest-anyio-0.0.0.tar.gz", hash = "sha256:b41234e9e9ad7ea1dbfefcc1d6891b23d5ef7c9f07ccf804c13a9cc338571fd3", size = 1560, upload-time = "2021-06-29T22:57:30.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/25/bd6493ae85d0a281b6a0f248d0fdb1d9aa2b31f18bcd4a8800cf397d8209/pytest_anyio-0.0.0-py2.py3-none-any.whl", hash = "sha256:dc8b5c4741cb16ff90be37fddd585ca943ed12bbeb563de7ace6cd94441d8746", size = 1999, upload-time = "2021-06-29T22:57:29.158Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cov"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage", extra = ["toml"] },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.14.14"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellingham"
|
||||||
|
version = "1.5.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy"
|
||||||
|
version = "2.0.45"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
asyncio = [
|
||||||
|
{ name = "greenlet" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "0.50.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ty"
|
||||||
|
version = "0.0.13"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "shellingham" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user