mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2026-01-17 11:40:39 +03:00
Merge pull request #231 from grillazz/switch-logger-to-rotoger
Switch logger to rotoger
This commit is contained in:
2
.env
2
.env
@@ -6,6 +6,8 @@ POSTGRES_HOST=postgres
|
|||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_DB=devdb
|
POSTGRES_DB=devdb
|
||||||
POSTGRES_USER=devdb
|
POSTGRES_USER=devdb
|
||||||
|
POSTGRES_TEST_DB=testdb
|
||||||
|
POSTGRES_TEST_USER=testdb
|
||||||
POSTGRES_PASSWORD=secret
|
POSTGRES_PASSWORD=secret
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
|
|||||||
10
.github/workflows/build-and-test.yml
vendored
10
.github/workflows/build-and-test.yml
vendored
@@ -41,13 +41,11 @@ jobs:
|
|||||||
POSTGRES_DB: testdb
|
POSTGRES_DB: testdb
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
# needed because the postgres container does not provide a health check
|
|
||||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- name: Checkout repository
|
||||||
- name: Create database schema
|
uses: actions/checkout@v5
|
||||||
run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;"
|
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v7
|
||||||
@@ -58,6 +56,4 @@ jobs:
|
|||||||
run: uv run --frozen ruff check .
|
run: uv run --frozen ruff check .
|
||||||
|
|
||||||
- name: Test with python ${{ matrix.python-version }}
|
- name: Test with python ${{ matrix.python-version }}
|
||||||
run: uv run --frozen pytest
|
run: uv run pytest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -45,7 +45,7 @@ docker-create-db-migration: ## Create a new alembic database migration. Example
|
|||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
.PHONY: docker-test
|
.PHONY: docker-test
|
||||||
docker-test: ## Run project tests
|
docker-test: ## Run project tests
|
||||||
docker compose -f compose.yml -f test-compose.yml run --rm api1 pytest tests --durations=0 -vv
|
docker compose -f compose.yml run --rm api1 pytest tests --durations=0 -vv
|
||||||
|
|
||||||
.PHONY: docker-test-snapshot
|
.PHONY: docker-test-snapshot
|
||||||
docker-test-snapshot: ## Run project tests and update snapshots
|
docker-test-snapshot: ## Run project tests and update snapshots
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class Settings(BaseSettings):
|
|||||||
POSTGRES_PASSWORD: str
|
POSTGRES_PASSWORD: str
|
||||||
POSTGRES_HOST: str
|
POSTGRES_HOST: str
|
||||||
POSTGRES_DB: str
|
POSTGRES_DB: str
|
||||||
|
POSTGRES_TEST_USER: str
|
||||||
|
POSTGRES_TEST_DB: str
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
@@ -80,6 +82,30 @@ class Settings(BaseSettings):
|
|||||||
path=self.POSTGRES_DB,
|
path=self.POSTGRES_DB,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def test_asyncpg_url(self) -> PostgresDsn:
|
||||||
|
"""
|
||||||
|
This is a computed field that generates a PostgresDsn URL for the test database using asyncpg.
|
||||||
|
|
||||||
|
The URL is built using the MultiHostUrl.build method, which takes the following parameters:
|
||||||
|
- scheme: The scheme of the URL. In this case, it is "postgresql+asyncpg".
|
||||||
|
- username: The username for the Postgres database, retrieved from the POSTGRES_USER environment variable.
|
||||||
|
- password: The password for the Postgres database, retrieved from the POSTGRES_PASSWORD environment variable.
|
||||||
|
- host: The host of the Postgres database, retrieved from the POSTGRES_HOST environment variable.
|
||||||
|
- path: The path of the Postgres test database, retrieved from the POSTGRES_TEST_DB environment variable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PostgresDsn: The constructed PostgresDsn URL for the test database with asyncpg.
|
||||||
|
"""
|
||||||
|
return MultiHostUrl.build(
|
||||||
|
scheme="postgresql+asyncpg",
|
||||||
|
username=self.POSTGRES_USER,
|
||||||
|
password=self.POSTGRES_PASSWORD,
|
||||||
|
host=self.POSTGRES_HOST,
|
||||||
|
path=self.POSTGRES_TEST_DB,
|
||||||
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def postgres_url(self) -> PostgresDsn:
|
def postgres_url(self) -> PostgresDsn:
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ engine = create_async_engine(
|
|||||||
echo=True,
|
echo=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_engine = create_async_engine(
|
||||||
|
global_settings.test_asyncpg_url.unicode_string(),
|
||||||
|
future=True,
|
||||||
|
echo=True,
|
||||||
|
)
|
||||||
|
|
||||||
# expire_on_commit=False will prevent attributes from being expired
|
# expire_on_commit=False will prevent attributes from being expired
|
||||||
# after commit.
|
# after commit.
|
||||||
AsyncSessionFactory = async_sessionmaker(
|
AsyncSessionFactory = async_sessionmaker(
|
||||||
@@ -23,6 +29,12 @@ AsyncSessionFactory = async_sessionmaker(
|
|||||||
expire_on_commit=False,
|
expire_on_commit=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TestAsyncSessionFactory = async_sessionmaker(
|
||||||
|
test_engine,
|
||||||
|
autoflush=False,
|
||||||
|
expire_on_commit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Dependency
|
# Dependency
|
||||||
async def get_db() -> AsyncGenerator:
|
async def get_db() -> AsyncGenerator:
|
||||||
@@ -38,3 +50,18 @@ async def get_db() -> AsyncGenerator:
|
|||||||
if not isinstance(ex, ResponseValidationError):
|
if not isinstance(ex, ResponseValidationError):
|
||||||
await logger.aerror(f"Database-related error: {repr(ex)}")
|
await logger.aerror(f"Database-related error: {repr(ex)}")
|
||||||
raise # Re-raise to be handled by appropriate handlers
|
raise # Re-raise to be handled by appropriate handlers
|
||||||
|
|
||||||
|
|
||||||
|
async def get_test_db() -> AsyncGenerator:
|
||||||
|
async with TestAsyncSessionFactory() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
await session.commit()
|
||||||
|
except SQLAlchemyError:
|
||||||
|
# Re-raise SQLAlchemy errors to be handled by the global handler
|
||||||
|
raise
|
||||||
|
except Exception as ex:
|
||||||
|
# Only log actual database-related issues, not response validation
|
||||||
|
if not isinstance(ex, ResponseValidationError):
|
||||||
|
await logger.aerror(f"Database-related error: {repr(ex)}")
|
||||||
|
raise # Re-raise to be handled by appropriate handlers
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# pull official base image
|
# pull official base image
|
||||||
FROM postgres:17.6-alpine
|
FROM postgres:17.6-alpine
|
||||||
|
|
||||||
# run create.sql on init
|
|
||||||
ADD create.sql /docker-entrypoint-initdb.d
|
|
||||||
|
|
||||||
WORKDIR /home/gx/code
|
WORKDIR /home/gx/code
|
||||||
|
|
||||||
COPY shakespeare_chapter.sql /home/gx/code/shakespeare_chapter.sql
|
COPY shakespeare_chapter.sql /home/gx/code/shakespeare_chapter.sql
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ dependencies = [
|
|||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
dev-dependencies = [
|
dev-dependencies = [
|
||||||
"ruff==0.14.9",
|
"ruff==0.14.10",
|
||||||
"devtools[pygments]==0.12.2",
|
"devtools[pygments]==0.12.2",
|
||||||
"pyupgrade==3.21.2",
|
"pyupgrade==3.21.2",
|
||||||
"ipython==9.8.0",
|
"ipython==9.8.0",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
services:
|
|
||||||
api1:
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=testdb
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER}
|
|
||||||
- POSTGRES_DB=testdb
|
|
||||||
@@ -3,8 +3,10 @@ from typing import Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.exc import ProgrammingError
|
||||||
|
|
||||||
from app.database import engine
|
from app.database import engine, get_db, get_test_db, test_engine
|
||||||
from app.main import app
|
from app.main import app
|
||||||
from app.models.base import Base
|
from app.models.base import Base
|
||||||
from app.redis import get_redis
|
from app.redis import get_redis
|
||||||
@@ -19,15 +21,46 @@ from app.redis import get_redis
|
|||||||
def anyio_backend(request):
|
def anyio_backend(request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
def _create_db(conn) -> None:
|
||||||
|
"""Create the test database if it doesn't exist."""
|
||||||
|
try:
|
||||||
|
conn.execute(text("CREATE DATABASE testdb"))
|
||||||
|
except ProgrammingError:
|
||||||
|
# This might be raised by databases that don't support `IF NOT EXISTS`
|
||||||
|
# and the schema already exists. You can choose to ignore it.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _create_db_schema(conn) -> None:
|
||||||
|
"""Create a database schema if it doesn't exist."""
|
||||||
|
try:
|
||||||
|
"""Create a database schema if it doesn't exist."""
|
||||||
|
conn.execute(text("CREATE SCHEMA IF NOT EXISTS happy_hog"))
|
||||||
|
conn.execute(text("CREATE SCHEMA IF NOT EXISTS shakespeare"))
|
||||||
|
except ProgrammingError:
|
||||||
|
# This might be raised by databases that don't support `IF NOT EXISTS`
|
||||||
|
# and the schema already exists. You can choose to ignore it.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def start_db():
|
async def start_db():
|
||||||
async with engine.begin() as conn:
|
# The `engine` is configured for the default 'postgres' database.
|
||||||
|
# We connect to it and create the test database.
|
||||||
|
# A transaction block is not used, as CREATE DATABASE cannot run inside it.
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
await conn.execute(text("COMMIT")) # Ensure we're not in a transaction
|
||||||
|
await conn.run_sync(_create_db)
|
||||||
|
|
||||||
|
# Now, connect to the newly created `testdb` with `test_engine`
|
||||||
|
async with test_engine.begin() as conn:
|
||||||
|
await conn.run_sync(_create_db_schema)
|
||||||
await conn.run_sync(Base.metadata.drop_all)
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
# for AsyncEngine created in function scope, close and
|
# for AsyncEngine created in function scope, close and
|
||||||
# clean-up pooled connections
|
# clean-up pooled connections
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
await test_engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -40,5 +73,6 @@ async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001
|
|||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
transport=transport,
|
transport=transport,
|
||||||
) as test_client:
|
) as test_client:
|
||||||
|
app.dependency_overrides[get_db] = get_test_db
|
||||||
app.redis = await get_redis()
|
app.redis = await get_redis()
|
||||||
yield test_client
|
yield test_client
|
||||||
|
|||||||
42
uv.lock
generated
42
uv.lock
generated
@@ -450,7 +450,7 @@ dev = [
|
|||||||
{ name = "devtools", extras = ["pygments"], specifier = "==0.12.2" },
|
{ name = "devtools", extras = ["pygments"], specifier = "==0.12.2" },
|
||||||
{ name = "ipython", specifier = "==9.8.0" },
|
{ name = "ipython", specifier = "==9.8.0" },
|
||||||
{ name = "pyupgrade", specifier = "==3.21.2" },
|
{ name = "pyupgrade", specifier = "==3.21.2" },
|
||||||
{ name = "ruff", specifier = "==0.14.9" },
|
{ name = "ruff", specifier = "==0.14.10" },
|
||||||
{ name = "tryceratops", specifier = "==2.4.1" },
|
{ name = "tryceratops", specifier = "==2.4.1" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1185,28 +1185,28 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.14.9"
|
version = "0.14.10"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" },
|
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" },
|
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" },
|
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user