mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-08-05 16:14:08 +00:00
Compare commits
11
Commits
v4.1.1
...
3e4ddc0100
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e4ddc0100 | ||
|
|
2641881df5
|
||
|
|
49b579bcec | ||
|
|
4bb4287922 | ||
|
|
43445e931e
|
||
|
|
27a36b0c82 | ||
|
|
d81adde685 | ||
|
|
c96779f10c | ||
|
|
880009dd9a | ||
|
|
3e2518b803 | ||
|
|
98328d4e20 |
+46
-18
@@ -1,6 +1,6 @@
|
||||
# Pytest
|
||||
|
||||
Testing helpers for FastAPI applications with async client, database sessions, and parallel worker support.
|
||||
Testing helpers for FastAPI applications: async HTTP client, database sessions, and parallel worker support.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -14,13 +14,9 @@ Testing helpers for FastAPI applications with async client, database sessions, a
|
||||
pip install "fastapi-toolsets[pytest]"
|
||||
```
|
||||
|
||||
## Overview
|
||||
## Async client
|
||||
|
||||
The `pytest` module provides utilities for setting up async test clients, managing test database sessions, and supporting parallel test execution with `pytest-xdist`.
|
||||
|
||||
## Creating an async client
|
||||
|
||||
Use [`create_async_client`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_async_client) to get an `httpx.AsyncClient` configured for your FastAPI app:
|
||||
Use [`create_async_client`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_async_client) to get an `httpx.AsyncClient` bound to your FastAPI app:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_async_client
|
||||
@@ -38,9 +34,20 @@ async def http_client(db_session):
|
||||
yield c
|
||||
```
|
||||
|
||||
## Database sessions in tests
|
||||
Any extra keyword arguments are forwarded to `httpx.AsyncClient`, so you can set default headers, authentication, timeouts, and more:
|
||||
|
||||
Use [`create_db_session`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_db_session) to create an isolated `AsyncSession` for a test, combined with [`create_worker_database`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_worker_database) to set up a per-worker database:
|
||||
```python
|
||||
async with create_async_client(
|
||||
app=app,
|
||||
headers={"X-Api-Key": "secret"},
|
||||
timeout=10,
|
||||
) as c:
|
||||
...
|
||||
```
|
||||
|
||||
## Database sessions
|
||||
|
||||
Use [`create_worker_database`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_worker_database) + [`create_db_session`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_db_session) to get a fully isolated `AsyncSession` for each test:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_worker_database, create_db_session
|
||||
@@ -61,25 +68,46 @@ async def db_session(worker_db_url):
|
||||
yield session
|
||||
```
|
||||
|
||||
`create_worker_database` connects without specifying a database (asyncpg falls back to the username), so the target test database does not need to exist beforehand.
|
||||
|
||||
!!! info
|
||||
In this example, the database is reset between each test using the argument `cleanup=True`.
|
||||
`cleanup=True` truncates all tables between tests via `TRUNCATE … RESTART IDENTITY CASCADE`, which is faster than dropping and recreating tables.
|
||||
|
||||
### Engine and session options
|
||||
|
||||
Pass `engine_kwargs` or `session_kwargs` to forward options to the underlying SQLAlchemy primitives:
|
||||
|
||||
```python
|
||||
async with create_db_session(
|
||||
database_url=worker_db_url,
|
||||
base=Base,
|
||||
engine_kwargs={"pool_size": 5, "connect_args": {"timeout": 10}},
|
||||
session_kwargs={"autoflush": False},
|
||||
) as session:
|
||||
...
|
||||
```
|
||||
|
||||
## Parallel testing with pytest-xdist
|
||||
|
||||
The fixtures above work with `pytest-xdist` out of the box. Each worker gets its own database named after the worker (e.g. `gw0`, `gw1`). Pass `prefix` to namespace the database (e.g. `prefix="myapp"` → `myapp_gw0`).
|
||||
|
||||
Use [`worker_database_url`](../reference/pytest.md#fastapi_toolsets.pytest.utils.worker_database_url) to derive the per-worker URL manually if needed:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import worker_database_url
|
||||
|
||||
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/test_db", default_test_db="test")
|
||||
# e.g. "postgresql+asyncpg://user:pass@localhost/test_db_gw0" under xdist
|
||||
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test")
|
||||
# → "postgresql+asyncpg://user:pass@localhost/gw0" under xdist
|
||||
# → "postgresql+asyncpg://user:pass@localhost/test" otherwise
|
||||
|
||||
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test", prefix="myapp")
|
||||
# → "postgresql+asyncpg://user:pass@localhost/myapp_gw0" under xdist
|
||||
# → "postgresql+asyncpg://user:pass@localhost/myapp_test" otherwise
|
||||
```
|
||||
|
||||
## Parallel testing with pytest-xdist
|
||||
## Manual table cleanup
|
||||
|
||||
The examples above are already compatible with parallel test execution with `pytest-xdist`.
|
||||
|
||||
## Cleaning up tables
|
||||
|
||||
If you want to manually clean up a database you can use [`cleanup_tables`](../reference/db.md#fastapi_toolsets.db.cleanup_tables), this will truncate all tables between tests for fast isolation:
|
||||
[`cleanup_tables`](../reference/db.md#fastapi_toolsets.db.cleanup_tables) truncates all tables in a single statement and can be called directly when you need more control:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.db import cleanup_tables
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "fastapi-toolsets"
|
||||
version = "4.1.1"
|
||||
version = "4.1.3"
|
||||
description = "Production-ready utilities for FastAPI applications"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -21,4 +21,4 @@ Example usage:
|
||||
return Response(data={"user": user.username}, message="Success")
|
||||
"""
|
||||
|
||||
__version__ = "4.1.1"
|
||||
__version__ = "4.1.3"
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.engine import make_url
|
||||
from sqlalchemy.engine import URL, make_url
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
@@ -34,12 +34,18 @@ def _get_xdist_worker(default_test_db: str) -> str:
|
||||
return os.environ.get("PYTEST_XDIST_WORKER", default_test_db)
|
||||
|
||||
|
||||
def worker_database_url(database_url: str, default_test_db: str) -> str:
|
||||
def worker_database_url(
|
||||
database_url: str,
|
||||
default_test_db: str,
|
||||
*,
|
||||
prefix: str | None = None,
|
||||
) -> str:
|
||||
"""Derive a per-worker database URL for pytest-xdist parallel runs.
|
||||
|
||||
Appends ``_{worker_name}`` to the database name so each xdist worker
|
||||
operates on its own database. When not running under xdist,
|
||||
``_{default_test_db}`` is appended instead.
|
||||
Sets the database name to the worker name so each xdist worker operates
|
||||
on its own database. When not running under xdist, *default_test_db* is
|
||||
used instead. When *prefix* is provided, the name becomes
|
||||
``{prefix}_{worker}``.
|
||||
|
||||
The worker name is read from the ``PYTEST_XDIST_WORKER`` environment
|
||||
variable (set automatically by xdist in each worker process).
|
||||
@@ -48,6 +54,9 @@ def worker_database_url(database_url: str, default_test_db: str) -> str:
|
||||
database_url: Original database connection URL.
|
||||
default_test_db: Suffix appended to the database name when
|
||||
``PYTEST_XDIST_WORKER`` is not set.
|
||||
prefix: Optional prefix prepended to the worker name
|
||||
(e.g. ``"test"`` → ``"test_gw0"``). Without it, the database
|
||||
name is just the worker name (e.g. ``"gw0"``).
|
||||
|
||||
Returns:
|
||||
A database URL with a worker- or default-specific database name.
|
||||
@@ -55,7 +64,8 @@ def worker_database_url(database_url: str, default_test_db: str) -> str:
|
||||
worker = _get_xdist_worker(default_test_db=default_test_db)
|
||||
|
||||
url = make_url(database_url)
|
||||
url = url.set(database=f"{url.database}_{worker}")
|
||||
db_name = f"{prefix}_{worker}" if prefix else worker
|
||||
url = url.set(database=db_name)
|
||||
return url.render_as_string(hide_password=False)
|
||||
|
||||
|
||||
@@ -63,6 +73,9 @@ def worker_database_url(database_url: str, default_test_db: str) -> str:
|
||||
async def create_worker_database(
|
||||
database_url: str,
|
||||
default_test_db: str = "test_db",
|
||||
*,
|
||||
prefix: str | None = None,
|
||||
server_url: str | None = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Create and drop a per-worker database for pytest-xdist isolation.
|
||||
|
||||
@@ -74,10 +87,16 @@ async def create_worker_database(
|
||||
name (e.g. ``_gw0``). Otherwise it is suffixed with *default_test_db*.
|
||||
|
||||
Args:
|
||||
database_url: Original database connection URL (used as the server
|
||||
connection and as the base for the worker database name).
|
||||
database_url: Original database connection URL (used as the base for
|
||||
the worker database name).
|
||||
default_test_db: Suffix appended to the database name when
|
||||
``PYTEST_XDIST_WORKER`` is not set. Defaults to ``"test_db"``.
|
||||
prefix: Optional prefix prepended to the worker name
|
||||
(e.g. ``prefix="test"`` → ``"test_gw0"``). Without it, the
|
||||
database name is just the worker name (e.g. ``"gw0"``).
|
||||
server_url: URL used for server-level DDL (must point to an existing
|
||||
database on the same server). Defaults to *database_url* with the
|
||||
database omitted, letting asyncpg fall back to the username.
|
||||
|
||||
Yields:
|
||||
The worker-specific database URL.
|
||||
@@ -86,7 +105,7 @@ async def create_worker_database(
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_worker_database, create_db_session
|
||||
|
||||
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/test_db"
|
||||
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/myapp"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def worker_db_url():
|
||||
@@ -102,21 +121,35 @@ async def create_worker_database(
|
||||
```
|
||||
"""
|
||||
worker_url = worker_database_url(
|
||||
database_url=database_url, default_test_db=default_test_db
|
||||
database_url=database_url, default_test_db=default_test_db, prefix=prefix
|
||||
)
|
||||
worker_db_name = make_url(worker_url).database
|
||||
assert worker_db_name is not None
|
||||
|
||||
engine = create_async_engine(database_url, isolation_level="AUTOCOMMIT")
|
||||
_parsed = make_url(database_url)
|
||||
_server_url = server_url or URL.create(
|
||||
drivername=_parsed.drivername,
|
||||
username=_parsed.username,
|
||||
password=_parsed.password,
|
||||
host=_parsed.host,
|
||||
port=_parsed.port,
|
||||
query=_parsed.query,
|
||||
).render_as_string(hide_password=False)
|
||||
|
||||
engine = create_async_engine(_server_url, isolation_level="AUTOCOMMIT")
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text(f"DROP DATABASE IF EXISTS {worker_db_name}"))
|
||||
await create_database(db_name=worker_db_name, server_url=database_url)
|
||||
await conn.execute(
|
||||
text(f"DROP DATABASE IF EXISTS {worker_db_name} WITH (FORCE)")
|
||||
)
|
||||
await create_database(db_name=worker_db_name, server_url=_server_url)
|
||||
|
||||
yield worker_url
|
||||
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text(f"DROP DATABASE IF EXISTS {worker_db_name}"))
|
||||
await conn.execute(
|
||||
text(f"DROP DATABASE IF EXISTS {worker_db_name} WITH (FORCE)")
|
||||
)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
@@ -126,6 +159,7 @@ async def create_async_client(
|
||||
app: Any,
|
||||
base_url: str = "http://test",
|
||||
dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""Create an async httpx client for testing FastAPI applications.
|
||||
|
||||
@@ -135,6 +169,9 @@ async def create_async_client(
|
||||
dependency_overrides: Optional mapping of original dependencies to
|
||||
their test replacements. Applied via ``app.dependency_overrides``
|
||||
before yielding and cleaned up after.
|
||||
**kwargs: Additional keyword arguments forwarded to
|
||||
:class:`httpx.AsyncClient` (e.g. ``headers``, ``cookies``,
|
||||
``auth``, ``timeout``).
|
||||
|
||||
Yields:
|
||||
An AsyncClient configured for the app.
|
||||
@@ -182,7 +219,9 @@ async def create_async_client(
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url=base_url) as client:
|
||||
async with AsyncClient(
|
||||
transport=transport, base_url=base_url, **kwargs
|
||||
) as client:
|
||||
yield client
|
||||
finally:
|
||||
if dependency_overrides:
|
||||
@@ -199,6 +238,8 @@ async def create_db_session(
|
||||
expire_on_commit: bool = False,
|
||||
drop_tables: bool = True,
|
||||
cleanup: bool = False,
|
||||
engine_kwargs: dict[str, Any] | None = None,
|
||||
session_kwargs: dict[str, Any] | None = None,
|
||||
) -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Create a database session for testing.
|
||||
|
||||
@@ -213,6 +254,12 @@ async def create_db_session(
|
||||
drop_tables: Drop tables after test. Defaults to True.
|
||||
cleanup: Truncate all tables after test using
|
||||
:func:`cleanup_tables`. Defaults to False.
|
||||
engine_kwargs: Additional keyword arguments forwarded to
|
||||
:func:`sqlalchemy.ext.asyncio.create_async_engine`
|
||||
(e.g. ``pool_size``, ``connect_args``).
|
||||
session_kwargs: Additional keyword arguments forwarded to
|
||||
:class:`sqlalchemy.ext.asyncio.async_sessionmaker`
|
||||
(e.g. ``autoflush``, ``class_``).
|
||||
|
||||
Yields:
|
||||
An AsyncSession ready for database operations.
|
||||
@@ -237,15 +284,17 @@ async def create_db_session(
|
||||
await db_session.commit()
|
||||
```
|
||||
"""
|
||||
engine = create_async_engine(database_url, echo=echo)
|
||||
engine = create_async_engine(database_url, echo=echo, **(engine_kwargs or {}))
|
||||
|
||||
try:
|
||||
# Create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(base.metadata.create_all)
|
||||
|
||||
session_maker = async_sessionmaker(
|
||||
engine, expire_on_commit=expire_on_commit, class_=EventSession
|
||||
engine,
|
||||
expire_on_commit=expire_on_commit,
|
||||
class_=EventSession,
|
||||
**(session_kwargs or {}),
|
||||
)
|
||||
async with session_maker() as session:
|
||||
yield session
|
||||
|
||||
+172
-10
@@ -278,6 +278,21 @@ class TestCreateAsyncClient:
|
||||
# Overrides should be cleaned up
|
||||
assert original_dep not in app.dependency_overrides
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_kwargs_forwarded_to_async_client(self):
|
||||
"""Extra kwargs are forwarded to AsyncClient (e.g. default headers)."""
|
||||
from fastapi import Request
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/headers")
|
||||
async def headers_endpoint(request: Request):
|
||||
return {"x-custom": request.headers.get("x-custom")}
|
||||
|
||||
async with create_async_client(app, headers={"X-Custom": "sentinel"}) as client:
|
||||
response = await client.get("/headers")
|
||||
assert response.json() == {"x-custom": "sentinel"}
|
||||
|
||||
|
||||
class TestCreateDbSession:
|
||||
"""Tests for create_db_session helper."""
|
||||
@@ -355,6 +370,22 @@ class TestCreateDbSession:
|
||||
result = await session.execute(select(Role))
|
||||
assert result.all() == []
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_engine_kwargs_forwarded(self):
|
||||
"""engine_kwargs are forwarded to create_async_engine."""
|
||||
async with create_db_session(
|
||||
DATABASE_URL, Base, engine_kwargs={"pool_pre_ping": True}
|
||||
) as session:
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_session_kwargs_forwarded(self):
|
||||
"""session_kwargs are forwarded to async_sessionmaker."""
|
||||
async with create_db_session(
|
||||
DATABASE_URL, Base, session_kwargs={"autoflush": False}
|
||||
) as session:
|
||||
assert session.autoflush is False
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_transaction_commits_visible_to_separate_session(self):
|
||||
"""Data written via get_transaction() is committed and visible to other sessions."""
|
||||
@@ -411,21 +442,19 @@ class TestGetXdistWorker:
|
||||
class TestWorkerDatabaseUrl:
|
||||
"""Tests for worker_database_url helper."""
|
||||
|
||||
def test_appends_default_test_db_without_xdist(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""default_test_db is appended when not running under xdist."""
|
||||
def test_uses_default_test_db_without_xdist(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""default_test_db is used as the database name when not running under xdist."""
|
||||
monkeypatch.delenv("PYTEST_XDIST_WORKER", raising=False)
|
||||
url = "postgresql+asyncpg://user:pass@localhost:5432/mydb"
|
||||
result = worker_database_url(url, default_test_db="fallback")
|
||||
assert make_url(result).database == "mydb_fallback"
|
||||
assert make_url(result).database == "fallback"
|
||||
|
||||
def test_appends_worker_id_to_database_name(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Worker name is appended to the database name."""
|
||||
def test_uses_worker_id_as_database_name(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Worker name is used as the database name."""
|
||||
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw0")
|
||||
url = "postgresql+asyncpg://user:pass@localhost:5432/db"
|
||||
result = worker_database_url(url, default_test_db="unused")
|
||||
assert make_url(result).database == "db_gw0"
|
||||
assert make_url(result).database == "gw0"
|
||||
|
||||
def test_preserves_url_components(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Host, port, username, password, and driver are preserved."""
|
||||
@@ -438,7 +467,21 @@ class TestWorkerDatabaseUrl:
|
||||
assert result.password == "secret"
|
||||
assert result.host == "dbhost"
|
||||
assert result.port == 6543
|
||||
assert result.database == "testdb_gw2"
|
||||
assert result.database == "gw2"
|
||||
|
||||
def test_prefix_with_xdist(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""prefix is prepended to the worker name when running under xdist."""
|
||||
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw0")
|
||||
url = "postgresql+asyncpg://user:pass@localhost:5432/mydb"
|
||||
result = worker_database_url(url, default_test_db="unused", prefix="myapp")
|
||||
assert make_url(result).database == "myapp_gw0"
|
||||
|
||||
def test_prefix_without_xdist(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""prefix is prepended to default_test_db when not running under xdist."""
|
||||
monkeypatch.delenv("PYTEST_XDIST_WORKER", raising=False)
|
||||
url = "postgresql+asyncpg://user:pass@localhost:5432/mydb"
|
||||
result = worker_database_url(url, default_test_db="test", prefix="myapp")
|
||||
assert make_url(result).database == "myapp_test"
|
||||
|
||||
|
||||
class TestCreateWorkerDatabase:
|
||||
@@ -448,7 +491,7 @@ class TestCreateWorkerDatabase:
|
||||
async def test_creates_default_db_without_xdist(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""Without xdist, creates a database suffixed with default_test_db."""
|
||||
"""Without xdist, creates a database named after default_test_db."""
|
||||
monkeypatch.delenv("PYTEST_XDIST_WORKER", raising=False)
|
||||
default_test_db = "no_xdist_default"
|
||||
expected_db = make_url(
|
||||
@@ -535,6 +578,125 @@ class TestCreateWorkerDatabase:
|
||||
assert result.scalar() is None
|
||||
await engine.dispose()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_works_when_database_url_db_does_not_exist(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""Succeeds even when the database named in database_url does not exist.
|
||||
|
||||
Regression test: the old code connected the DDL engine to database_url
|
||||
itself, which failed when that database had not been created yet.
|
||||
"""
|
||||
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw_noexist")
|
||||
nonexistent_url = (
|
||||
make_url(DATABASE_URL)
|
||||
.set(database="no_such_db")
|
||||
.render_as_string(hide_password=False)
|
||||
)
|
||||
expected_db = make_url(
|
||||
worker_database_url(nonexistent_url, default_test_db="unused")
|
||||
).database
|
||||
|
||||
async with create_worker_database(nonexistent_url) as url:
|
||||
assert make_url(url).database == expected_db
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, isolation_level="AUTOCOMMIT")
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
||||
{"name": expected_db},
|
||||
)
|
||||
assert result.scalar() == 1
|
||||
await engine.dispose()
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, isolation_level="AUTOCOMMIT")
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
||||
{"name": expected_db},
|
||||
)
|
||||
assert result.scalar() is None
|
||||
await engine.dispose()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_explicit_server_url(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Explicit server_url is used instead of the auto-derived one."""
|
||||
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw_explicit_srv")
|
||||
expected_db = make_url(
|
||||
worker_database_url(DATABASE_URL, default_test_db="unused")
|
||||
).database
|
||||
|
||||
async with create_worker_database(DATABASE_URL, server_url=DATABASE_URL) as url:
|
||||
assert make_url(url).database == expected_db
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, isolation_level="AUTOCOMMIT")
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
||||
{"name": expected_db},
|
||||
)
|
||||
assert result.scalar() == 1
|
||||
await engine.dispose()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_drops_database_with_active_connections(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""DROP DATABASE succeeds even when a connection is still open to it."""
|
||||
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw_active_conn")
|
||||
expected_db = make_url(
|
||||
worker_database_url(DATABASE_URL, default_test_db="unused")
|
||||
).database
|
||||
|
||||
lingering_engine = None
|
||||
async with create_worker_database(DATABASE_URL) as url:
|
||||
# Open a connection to the worker DB and intentionally leave it open.
|
||||
lingering_engine = create_async_engine(url)
|
||||
async with lingering_engine.connect():
|
||||
pass # connection returned to pool but engine not disposed
|
||||
|
||||
# If WITH (FORCE) is absent the DROP above would raise; reaching here means it worked.
|
||||
engine = create_async_engine(DATABASE_URL, isolation_level="AUTOCOMMIT")
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
||||
{"name": expected_db},
|
||||
)
|
||||
assert result.scalar() is None
|
||||
await engine.dispose()
|
||||
if lingering_engine:
|
||||
await lingering_engine.dispose()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_prefix_names_database(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""prefix is prepended to the worker name in the created database."""
|
||||
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw_prefix")
|
||||
expected_db = make_url(
|
||||
worker_database_url(DATABASE_URL, default_test_db="unused", prefix="pfx")
|
||||
).database
|
||||
assert expected_db == "pfx_gw_prefix"
|
||||
|
||||
async with create_worker_database(DATABASE_URL, prefix="pfx") as url:
|
||||
assert make_url(url).database == expected_db
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, isolation_level="AUTOCOMMIT")
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
||||
{"name": expected_db},
|
||||
)
|
||||
assert result.scalar() == 1
|
||||
await engine.dispose()
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, isolation_level="AUTOCOMMIT")
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
||||
{"name": expected_db},
|
||||
)
|
||||
assert result.scalar() is None
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
class _LocalBase(DeclarativeBase):
|
||||
pass
|
||||
|
||||
@@ -314,7 +314,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.1"
|
||||
version = "0.136.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -323,14 +323,14 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-toolsets"
|
||||
version = "4.1.1"
|
||||
version = "4.1.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "asyncpg" },
|
||||
@@ -1165,27 +1165,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.14"
|
||||
version = "0.15.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1262,15 +1262,15 @@ asyncio = [
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.50.0"
|
||||
version = "1.0.1"
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1329,32 +1329,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.38"
|
||||
version = "0.0.47"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/3b/45be6b37d5060d6917bf7f1f234c00d360fc5f8b7486f8a96af640e25661/ty-0.0.38.tar.gz", hash = "sha256:fbc8d47f7630457669ab41e333dc093897fdb7ead1ffc94dcf8f30b5d39aa56d", size = 5681218, upload-time = "2026-05-20T00:15:32.781Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/ea/2565229df2be7cc09128a1bb2fa8416b2006378b62bc8a3ddcc001e6d52f/ty-0.0.47.tar.gz", hash = "sha256:4db42f7a9b606860c98a25adb73059942016cee43d9f1cac1b63ee06063f4879", size = 5849321, upload-time = "2026-06-10T14:51:17.859Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/43/ea9b4e57d6a266670dbe34858e92f6093ca054ad1b48f1c82580a72340fb/ty-0.0.38-py3-none-linux_armv6l.whl", hash = "sha256:3501dcf44ca03f813f9cb4fabfdf601adc0ac1337c411405b470530679e37a45", size = 11289326, upload-time = "2026-05-20T00:14:52.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/ff/24e2f623a1c6b5f5ccf8bf82fccd937033c6a7dba57a4028c7f41270fa4a/ty-0.0.38-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b34b4094b76252c3e8c90762cdd5e8a9f1101534484745ff4b480f71eb38ac2e", size = 11063047, upload-time = "2026-05-20T00:14:42.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/41/4f0d910f0acbd20b358eda80a5cd6a8361d27ff5b8e87ab559d3f69f125e/ty-0.0.38-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c518ad33a877677365baab2e21d82cf59ffee789203a15a143f5179ee5a1d3f8", size = 10494436, upload-time = "2026-05-20T00:15:24.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d8/da06833422082aa98b169a391f9197e2d73865e96c90b6979ac886b890a2/ty-0.0.38-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9238494722303eccddc6a27eb647948b694eecd6b974910d13b9e6cd46bbeb6a", size = 11000992, upload-time = "2026-05-20T00:14:58.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f7/e1172197fb827e6410ca3eb0dc68ef2789f3c70683696f2a0ce5c90764fd/ty-0.0.38-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d91d7336c5d51bf822ac0df512f300584ca4dcca041fc6a6d7df03a8ddbb31", size = 11058583, upload-time = "2026-05-20T00:15:11.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/61/7fbaf0c05981e006a8804287819c574dff90a6bf8e96efad7226be0700aa/ty-0.0.38-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65165879814993450710b9349791e4898c65e36b1e14eec554884c06a2f20ff1", size = 11531036, upload-time = "2026-05-20T00:15:14.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e3/47c0c64e401d50f925df3e52479d4e7626754b2a9e38201d142fdacd6252/ty-0.0.38-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d61868b8d1c4033bf8088191de953fed245c2f9e1bb9d2d53e5699170b0924c", size = 12129991, upload-time = "2026-05-20T00:14:39.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/99/2f452d02901bcd7f1b109cf5b848727ce37f372c3406143aa52d1305d40e/ty-0.0.38-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f9a9175548c98dbff7707865738c07c2b1f8e07a09b8c68101baebb5dac59a4", size = 11756167, upload-time = "2026-05-20T00:15:27.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/0c/c7e14d111c813e1a20b82e944f1c997c4631a2bb710eaa64fb6b26835e13/ty-0.0.38-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375d3a964c6b4aea2e9237fdb5eb9ed03dc43088986a94209a28a4ea3b62001c", size = 11637099, upload-time = "2026-05-20T00:15:21.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/de/ab02659dd1ed62898db7db4d37f9937c80854dd45e95093fa0fe10328d82/ty-0.0.38-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cdfd547782c45267aa0b52abad31bd406bf4768c264532ef9e2360cd3c6ce048", size = 11813583, upload-time = "2026-05-20T00:14:45.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/57/bd1b5ebf4e71a4295484afac0202df1740b0807762b86744b1bef4534984/ty-0.0.38-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc675b75626470abe4e6c3b3934b853642b04f2ac4d7139fcefea3b48b213", size = 10975405, upload-time = "2026-05-20T00:15:30.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/55/0305c78711bbd23922cf291996a08ef9544f4179da98e9a75c14e608f379/ty-0.0.38-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54be4f00432870da42cd74fe145a3362fd248e22d032c74bd807cb45bf068f94", size = 11097551, upload-time = "2026-05-20T00:14:55.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/4f/7effe7f9a6ac9719eb7234172c01739c5f888bb47f9acc2ea8da1f4afed3/ty-0.0.38-py3-none-musllinux_1_2_i686.whl", hash = "sha256:494af66a76a86dbf16a3003d3b63b03484aa4c7489dfe11f3ee5413b98b22d60", size = 11214391, upload-time = "2026-05-20T00:15:18.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/cd/d9fdfec3a74a6ad0209fa5e7113ae29d4f457d0651cfbb813b4c6563e0d4/ty-0.0.38-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3d92527c4be78a5ce6d32e8bb0aa2a6988d4076eddf1294e56fdaf06d1a98e7e", size = 11730871, upload-time = "2026-05-20T00:14:49.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/4a/beefade12d109b4f7793d61b04b4478b1ad4d1465a719e7ff55b2d42461a/ty-0.0.38-py3-none-win32.whl", hash = "sha256:36fc5dd5dc09207ff3004b1560a79a3fb8d12456daeec914a7b802a918da654c", size = 10548583, upload-time = "2026-05-20T00:15:07.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/64/941b205e2e46cc2297c245c64aa7691410b7454fa4d07a6cb3cf59487833/ty-0.0.38-py3-none-win_amd64.whl", hash = "sha256:eef0a8956ba14514076b1a963d13eb32986d9ebad7f0527b3cc01cb68bf35147", size = 11650542, upload-time = "2026-05-20T00:15:01.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/02/c1c4f9ec4b94d95190636fa13f79c32f65165fbe3a0503882d4df164d2ac/ty-0.0.38-py3-none-win_arm64.whl", hash = "sha256:79abfc8658a026c30b1c955613437dab3ef4b12feca56a3e6df50903cc39e07f", size = 11010307, upload-time = "2026-05-20T00:15:04.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ae/e1358a2bdff2c64587be8f48aadd7b174414f7614a12757e4dcec298041c/ty-0.0.47-py3-none-linux_armv6l.whl", hash = "sha256:7242ff9bc6a409db623b3dd3c8578877e5f9e75fd35ae62484dfce14dd17a1c4", size = 11771785, upload-time = "2026-06-10T14:51:22.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/7a/67094fb6a85863307f617739a0f6dfaea87f85efc1d97327304d95de6e82/ty-0.0.47-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d08701aaaed49e44568e086b9e24da9723e523d2985a65c15b42cbeaec800bf4", size = 11514823, upload-time = "2026-06-10T14:51:10.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/96/5f7a20a804d0408655d8974cb78b6f9c096450132460aa7e942a800cb0a7/ty-0.0.47-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b6a03c57f04e9dfc6ffa328b1019dd3314bc350edc078417e49c8e766a90ddc3", size = 10924148, upload-time = "2026-06-10T14:51:20.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/d9/e2f98480fffa661d038d72c13c563efa22624404f324853d2d123467c7a4/ty-0.0.47-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3467db1f470ad05a150a422fe45132c9248b2efecdd6a39726c2e430d99a1a1", size = 11454020, upload-time = "2026-06-10T14:51:33.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/28/892ece7491c6f8abbf09d2ae7e903a985de7522227516a8ca7a0b634749c/ty-0.0.47-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b8e0e4dc34468821476531631366aa81f0ffe98d768200d99ec1ff830949ee8", size = 11545624, upload-time = "2026-06-10T14:50:57.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f1/d03d586e3baa9aca4f819f19bd97215a5ef10b135b72e7502ec478a13a7c/ty-0.0.47-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a70ffd1b5fc91e6ba2ade98fa324a1336848fead8d622612e3b1bbdd3a2c71f", size = 12069283, upload-time = "2026-06-10T14:51:00.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/76/9e4875a130279a86d1301e916ff8b520de48b2c2603cd4297417c39c34f8/ty-0.0.47-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a25e7bc280b6dd31e97642f3c68d2acfcbc8511af4ee2355f7cafa7dfef92fb", size = 12620386, upload-time = "2026-06-10T14:50:49.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/9e/ad31b6e67564815d8082c9934ab1e568acbbbd48c0efff00b864f1d1475d/ty-0.0.47-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7c583070eaca9788a03a36784f4359f759094c64cdbafd0731881397fe80246", size = 12295012, upload-time = "2026-06-10T14:51:25.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/2c/8ec68ccde675a339c29598a084543df79e2e1c9d6acb819744de0c382ae6/ty-0.0.47-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0bb02d7857f2736425758310a8783e0a85d151ccf2b6d2707241bb6b6fa5e9", size = 12118299, upload-time = "2026-06-10T14:51:13.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c4/947def605a7f5d5d81d73ea2913a8845d1e225eddba11cf80f33de78d78a/ty-0.0.47-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e3f36c265bafb44327c9bc1e77403edb34ef01c151e2193115ce1273b61a0056", size = 12334477, upload-time = "2026-06-10T14:51:29.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/2b/0c378342f79bb9bd640740d2afff6da5988bdc5f790081d7fd817f3b7c56/ty-0.0.47-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4ad371d660aee483b5f78b1f767b4213e4d058b3ff52f90f1586b77981dddce4", size = 11426077, upload-time = "2026-06-10T14:51:08.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/22/c4cfb8e48c57398b44206395675fdf738341b6efb20636d0dd894c2829e0/ty-0.0.47-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e79bb494b2f4fdf0a0b1a7db107063942852975368941ccc133d06fc627bd592", size = 11590725, upload-time = "2026-06-10T14:51:03.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/92/1a45f4866108bf94ff0322cce0b95f9ef5d7fbba5a08a572b90db98e87e4/ty-0.0.47-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d263c8b682aa06d56e34df9303ea256ebdfad34719d4092c5dd7997816e93e71", size = 11714791, upload-time = "2026-06-10T14:50:52.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/70/29ac8cb291af5c9ad6c69d25389f9657c7db0caadcb56b4d8fedd2178c67/ty-0.0.47-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:87488c8b9e8dd2de5733c68d4a305d989451348012a085f1179daa1f2f841a4f", size = 12205131, upload-time = "2026-06-10T14:50:54.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/e0/ad21d1647aefc3434d29f93ba90522540a55774228a25c4d9861655797b3/ty-0.0.47-py3-none-win32.whl", hash = "sha256:8e8fb0b27f3a4f1644101d57c1b3fe9cd72e303b37b5cf9b0015a45d54ce2efe", size = 11018686, upload-time = "2026-06-10T14:51:31.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/21/773e02fd3a6abe9fefe36e61ec3d058e1006299c2ae25f379cb27a0e5186/ty-0.0.47-py3-none-win_amd64.whl", hash = "sha256:2ca1d496bc4bf4eb7fd97e20d1233f524f70a252a3c8b68d115bfb9584a5a8d8", size = 12112283, upload-time = "2026-06-10T14:51:15.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/a3/a522dc6d16244f81e2b09bf5c06a83c4469ee412a2ab9764037a894988b0/ty-0.0.47-py3-none-win_arm64.whl", hash = "sha256:d3b3b0ea7f8cbb202ec1eb237b58210f33f4f5b07e06c67e46266b17f39ac8ab", size = 11460794, upload-time = "2026-06-10T14:51:05.774Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.26.2"
|
||||
version = "0.26.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -1362,9 +1362,9 @@ dependencies = [
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/a5/756f2e6bc81a7dd79aa3c625dd01b74cabc4516628cace2caaec09ca6ff2/typer-0.26.2.tar.gz", hash = "sha256:9b4f19e08fcc9427a822d1ef467b1fe76737a2f65c7926bdeba2337d73569b68", size = 198991, upload-time = "2026-05-27T10:41:39.166Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/ed/ef06584ccdd5c410df0837951ecd7e15d9a6144ea1bd4c73cecab1a89891/typer-0.26.7.tar.gz", hash = "sha256:e314a34c617e419c091b2830dda3ea1f257134ff593061a8f5b9717ab8dddb3a", size = 201709, upload-time = "2026-06-03T07:18:06.843Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/a5/6ffd702beda8798b2b82ff70805ed4a66d963557e43a5d1823ab456251a4/typer-0.26.2-py3-none-any.whl", hash = "sha256:39beff72ffbb31978a5b545f677d57edb97c6f980f433b38556deb0af25f094d", size = 123123, upload-time = "2026-05-27T10:41:40.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/2201973529af2c954de0bb725323c3aaed6d7f0ceee8f550dec9185df013/typer-0.26.7-py3-none-any.whl", hash = "sha256:5c87cfbc5d34491c5346ebf49c23e18d56ccb863268d3a8d592b26087c2f5e58", size = 122456, upload-time = "2026-06-03T07:18:05.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user