Merge pull request #231 from grillazz/switch-logger-to-rotoger

Switch logger to rotoger
This commit is contained in:
Ordinary Hobbit
2025-12-28 19:09:07 +01:00
committed by GitHub
10 changed files with 117 additions and 44 deletions

2
.env
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -1,9 +0,0 @@
services:
api1:
environment:
- POSTGRES_DB=testdb
postgres:
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=testdb

View File

@@ -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
View File

@@ -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]]