From 38294c56cfd5e61c96c5b3be680266caec154b64 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:38:54 +0100 Subject: [PATCH 01/14] feat: exceptions --- app/core/exceptions.py | 167 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 app/core/exceptions.py diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..f0a5e88 --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,167 @@ +class FlintException(Exception): + """ + Base exception for all Flint domain errors. + """ + + def __init__(self, message: str, status_code: int = 400) -> None: + self.message = message + self.status_code = status_code + super().__init__(message) + + +class JobNotFoundException(FlintException): + """ + Raised when a requested job does not exist or has been hard-deleted. + """ + + def __init__(self, job_id: str) -> None: + super().__init__( + message=f"Job '{job_id}' not found.", + status_code=404, + ) + + +class JobNotCancellableException(FlintException): + """ + Raised when a cancel request is made on a job in a terminal state + (completed, failed, cancelled). + """ + + def __init__(self, job_id: str, status: str) -> None: + super().__init__( + message=( + f"Job '{job_id}' cannot be cancelled because it is already '{status}'. " + "Only pending and processing jobs can be cancelled." + ), + status_code=409, + ) + + +class JobNotDeletableException(FlintException): + """ + Raised when a soft-delete is attempted on a job that is still + pending or processing. + """ + + def __init__(self, job_id: str, status: str) -> None: + super().__init__( + message=( + f"Job '{job_id}' cannot be deleted while it is '{status}'. " + "Cancel the job first, or wait for it to reach a terminal state." + ), + status_code=409, + ) + + +class JobAlreadyProcessingException(FlintException): + """ + Raised when two workers attempt to claim the same job. + """ + + def __init__(self, job_id: str) -> None: + super().__init__( + message=f"Job '{job_id}' is already being processed by another worker.", + status_code=409, + ) + + +class JobNotInBinException(FlintException): + """ + Raised when a restore or hard-delete is attempted on a non-deleted job. + """ + + def __init__(self, job_id: str) -> None: + super().__init__( + message=f"Job '{job_id}' is not in the bin.", + status_code=404, + ) + + +class JobNotInDLQException(FlintException): + """ + Raised when a DLQ retry is attempted on a job that is not in the DLQ. + """ + + def __init__(self, job_id: str) -> None: + super().__init__( + message=f"Job '{job_id}' is not in the dead letter queue.", + status_code=404, + ) + + +class DependencyCycleException(FlintException): + """ + Raised when adding a dependency would create a cycle in the + job dependency graph. + """ + + def __init__(self, job_id: str, dependency_id: str) -> None: + super().__init__( + message=( + f"Adding dependency '{dependency_id}' to job '{job_id}' " + "would create a cycle in the dependency graph. " + "Circular dependencies are not allowed." + ), + status_code=422, + ) + + +class DependencyNotFoundException(FlintException): + """Raised when a specified dependency job ID does not exist.""" + + def __init__(self, dependency_id: str) -> None: + super().__init__( + message=f"Dependency job '{dependency_id}' not found.", + status_code=404, + ) + + +class HandlerNotFoundException(FlintException): + """ + Raised when no handler is registered for a given job type. + """ + + def __init__(self, job_type: str) -> None: + super().__init__( + message=( + f"No handler registered for job type '{job_type}'. " + "Supported types: send_email, webhook_delivery, log_processing." + ), + status_code=422, + ) + + +class InvalidIntervalException(FlintException): + """Raised when an interval string cannot be parsed.""" + + def __init__(self, interval: str) -> None: + super().__init__( + message=( + f"Invalid interval format: '{interval}'. " + "Use where unit is one of: s, m, h, d, mo, y. " + "Examples: '30s', '5m', '2h', '1d', '1mo', '1y'." + ), + status_code=422, + ) + + +class SettingNotFoundException(FlintException): + """ + Raised when a requested settings key does not exist. + """ + + def __init__(self, key: str) -> None: + super().__init__( + message=f"Setting '{key}' not found.", + status_code=404, + ) + + +class WorkerNotFoundException(FlintException): + """Raised when a control action targets an unknown worker ID.""" + + def __init__(self, worker_id: str) -> None: + super().__init__( + message=f"Worker '{worker_id}' not found or is no longer active.", + status_code=404, + ) From a0576a22bfcb7b3321cef44650cb1f87baaeb12b Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:39:02 +0100 Subject: [PATCH 02/14] feat: app exception handlers --- app/core/exception_handlers.py | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/core/exception_handlers.py diff --git a/app/core/exception_handlers.py b/app/core/exception_handlers.py new file mode 100644 index 0000000..7902791 --- /dev/null +++ b/app/core/exception_handlers.py @@ -0,0 +1,88 @@ +from fastapi import FastAPI, HTTPException, Request, status +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +from app.core.exceptions import FlintException +from app.core.logger import get_logger +from app.schemas.response import ApiResponse, ErrorDetail + +logger = get_logger(__name__) + + +def register_exception_handlers(app: FastAPI) -> None: + app.add_exception_handler(FlintException, flint_exception_handler) # type: ignore + app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore + app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore + app.add_exception_handler(Exception, fallback_exception_handler) + + +async def flint_exception_handler( + request: Request, + exc: FlintException, +) -> JSONResponse: + logger.warning( + "domain_exception", + path=request.url.path, + status_code=exc.status_code, + error=exc.message, + ) + return JSONResponse( + status_code=exc.status_code, + content=ApiResponse[None]( + message=exc.message, + errors=[ErrorDetail(message=exc.message)], + ).model_dump(mode="json"), + ) + + +async def http_exception_handler( + request: Request, + exc: HTTPException, +) -> JSONResponse: + message = str(exc.detail) if exc.detail else "HTTP error" + return JSONResponse( + status_code=exc.status_code, + content=ApiResponse[None]( + message=message, + errors=[ErrorDetail(message=message)], + ).model_dump(mode="json"), + headers=exc.headers, + ) + + +async def validation_exception_handler( + request: Request, + exc: RequestValidationError, +) -> JSONResponse: + errors = [ + ErrorDetail( + field=".".join(str(part) for part in error["loc"]), + message=str(error["msg"]), + ) + for error in exc.errors() + ] + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=ApiResponse[None]( + message="Validation failed", + errors=errors, + ).model_dump(mode="json"), + ) + + +async def fallback_exception_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + logger.exception( + "unhandled_exception", + path=request.url.path, + error=str(exc), + ) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ApiResponse[None]( + message="Internal server error", + errors=[ErrorDetail(message="An unexpected error occurred.")], + ).model_dump(mode="json"), + ) From 3b1bd38ac79fc3cf34a61bea30f040d56a682551 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:39:12 +0100 Subject: [PATCH 03/14] feat: app logger module --- app/core/logger.py | 108 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/core/logger.py diff --git a/app/core/logger.py b/app/core/logger.py new file mode 100644 index 0000000..cbac51e --- /dev/null +++ b/app/core/logger.py @@ -0,0 +1,108 @@ +import logging +import os +import sys +from logging.handlers import RotatingFileHandler + +import structlog + + +def setup_logging() -> None: + """ + Configure structlog for structured JSON logging. + Output goes to both stdout and a rotating log file. + Called once at application startup in app/main.py. + """ + from app.core.config import settings + + log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO) + + log_dir = os.path.dirname(settings.LOG_FILE) + if log_dir: + os.makedirs(log_dir, exist_ok=True) + + shared_processors = [ + structlog.contextvars.merge_contextvars, + structlog.processors.add_log_level, + structlog.processors.TimeStamper(fmt="iso", utc=True), + structlog.processors.StackInfoRenderer(), + structlog.dev.set_exc_info, + _scrub_sensitive_fields, + ] + + structlog.configure( + processors=shared_processors + + [ + structlog.processors.JSONRenderer(), + ], + wrapper_class=structlog.make_filtering_bound_logger(log_level), + logger_factory=structlog.PrintLoggerFactory(), + cache_logger_on_first_use=True, + ) + + # File handler — rotating at 10MB, keeping 5 backups + file_handler = RotatingFileHandler( + settings.LOG_FILE, + maxBytes=10 * 1024 * 1024, + backupCount=5, + encoding="utf-8", + ) + file_handler.setLevel(log_level) + file_handler.setFormatter(logging.Formatter("%(message)s")) + + # Stdout handler + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setLevel(log_level) + stdout_handler.setFormatter(logging.Formatter("%(message)s")) + + # Root logger — captures structlog output and SQLAlchemy warnings + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # Avoid adding duplicate handlers on reload + if not root_logger.handlers: + root_logger.addHandler(stdout_handler) + root_logger.addHandler(file_handler) + + # Quieten noisy third-party loggers + logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) + logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING) + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("aiosmtplib").setLevel(logging.WARNING) + + +def _scrub_sensitive_fields( + logger: object, + method: str, + event_dict: dict, +) -> dict: + """ + Processor that removes sensitive keys from log entries + before they are written to stdout or file. + """ + _SENSITIVE_KEYS = frozenset( + { + "api_key", + "password", + "secret", + "token", + "smtp_password", + "authorization", + "x-api-key", + } + ) + for key in list(event_dict.keys()): + if key.lower() in _SENSITIVE_KEYS: + event_dict[key] = "***REDACTED***" + return event_dict + + +def get_logger(name: str) -> structlog.BoundLogger: + """ + Returns a structlog bound logger for the given module name. + Usage: + from app.core.logger import get_logger + logger = get_logger(__name__) + logger.info("job_created", job_id=str(job.id), type=job.type) + """ + return structlog.get_logger(name) From 6b79cbddf444ee4182e6401c55c964485b6fd6c6 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:39:19 +0100 Subject: [PATCH 04/14] feat: api key header check --- app/core/__init__.py | 0 app/core/security.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 app/core/__init__.py create mode 100644 app/core/security.py diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..c56702d --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,15 @@ +from fastapi import HTTPException, Security, status +from fastapi.security import APIKeyHeader + +from app.core.config import settings + +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + + +async def verify_api_key(api_key: str | None = Security(api_key_header)) -> str: + if not api_key or api_key != settings.API_KEY: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing API key", + ) + return api_key From 60491e99f7d2510636289d471c5ceb0938c8705e Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:39:25 +0100 Subject: [PATCH 05/14] feat: redis client helper --- app/db/redis.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 app/db/redis.py diff --git a/app/db/redis.py b/app/db/redis.py new file mode 100644 index 0000000..4d65d4a --- /dev/null +++ b/app/db/redis.py @@ -0,0 +1,31 @@ +import redis.asyncio as aioredis +from redis.asyncio import Redis + +from app.core.config import settings + +_redis_client: Redis | None = None + + +async def get_redis() -> Redis: + """ + Returns the shared async Redis client. + Creates the client on first call using REDIS_URL from settings. + """ + global _redis_client + if _redis_client is None: + _redis_client = aioredis.from_url( + settings.REDIS_URL, + decode_responses=True, + encoding="utf-8", + ) + return _redis_client + + +async def close_redis() -> None: + """ + Cleanly closes the Redis connection pool. + """ + global _redis_client + if _redis_client is not None: + await _redis_client.aclose() + _redis_client = None From 42d1924ce5dc5b49a60a78a22a979119c959881b Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:40:36 +0100 Subject: [PATCH 06/14] feat: base job handler --- app/handlers/__init__.py | 8 ++++++++ app/handlers/base.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 app/handlers/__init__.py create mode 100644 app/handlers/base.py diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py new file mode 100644 index 0000000..c9cbeb2 --- /dev/null +++ b/app/handlers/__init__.py @@ -0,0 +1,8 @@ +HANDLER_REGISTRY = {} + + +def get_handler(job_type: str): + handler_class = HANDLER_REGISTRY.get(job_type) + if not handler_class: + raise ValueError(f"No handler registered for job type: {job_type}") + return handler_class() diff --git a/app/handlers/base.py b/app/handlers/base.py new file mode 100644 index 0000000..d503f2e --- /dev/null +++ b/app/handlers/base.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod +from typing import Any + + +class BaseHandler(ABC): + @abstractmethod + async def execute(self, payload: dict[str, Any]) -> dict[str, Any]: + """ + Execute the job logic. + """ + ... + + @property + def handler_name(self) -> str: + """Human-readable name for logging.""" + return self.__class__.__name__ From 1d0993a346f689af61ba133497ac429cd86bb935 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:41:02 +0100 Subject: [PATCH 07/14] feat: api response schema envelope --- app/schemas/__init__.py | 0 app/schemas/response.py | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/response.py diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/response.py b/app/schemas/response.py new file mode 100644 index 0000000..73b5f06 --- /dev/null +++ b/app/schemas/response.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, Field + + +class Meta(BaseModel): + page: int | None = None + limit: int | None = None + total: int | None = None + + +class ErrorDetail(BaseModel): + field: str | None = None + message: str + + +class ApiResponse[T](BaseModel): + message: str + data: T | None = None + errors: list[ErrorDetail] = Field(default_factory=list) + meta: Meta | None = None From c7c385e31477e548162abcd12ad393ecec08a7c1 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:41:14 +0100 Subject: [PATCH 08/14] feat: depenedencies --- app/dependencies.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/dependencies.py diff --git a/app/dependencies.py b/app/dependencies.py new file mode 100644 index 0000000..a6ba72f --- /dev/null +++ b/app/dependencies.py @@ -0,0 +1,8 @@ +from typing import Annotated + +from fastapi import Depends +from sqlalchemy.ext.asyncio.session import AsyncSession + +from app.db.session import get_db + +DBSession = Annotated[AsyncSession, Depends(get_db)] From 0df3c311c1dbb26ec9973496a28dfdf961069388 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:42:19 +0100 Subject: [PATCH 09/14] chore: folder structure --- app/api/__init__.py | 0 app/api/v1/__init__.py | 1 + app/api/v1/router.py | 5 +++++ app/queues/__init__.py | 1 + app/services/__init__.py | 0 scheduler/__init__.py | 1 + tests/conftest.py | 6 ++++++ tests/integration/__init__.py | 1 + tests/unit/__init__.py | 1 + worker/__init__.py | 0 10 files changed, 16 insertions(+) create mode 100644 app/api/__init__.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/router.py create mode 100644 app/queues/__init__.py create mode 100644 app/services/__init__.py create mode 100644 scheduler/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 worker/__init__.py diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..2b17853 --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1 @@ +"""Version 1 API routes.""" diff --git a/app/api/v1/router.py b/app/api/v1/router.py new file mode 100644 index 0000000..0649154 --- /dev/null +++ b/app/api/v1/router.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter, Depends + +from app.core.security import verify_api_key + +api_router = APIRouter(dependencies=[Depends(verify_api_key)]) diff --git a/app/queues/__init__.py b/app/queues/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/queues/__init__.py @@ -0,0 +1 @@ + diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scheduler/__init__.py b/scheduler/__init__.py new file mode 100644 index 0000000..3497bab --- /dev/null +++ b/scheduler/__init__.py @@ -0,0 +1 @@ +"""Scheduler process package.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..af7e479 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def anyio_backend(): + return "asyncio" diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..c210fac --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests.""" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e0310a0 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/worker/__init__.py b/worker/__init__.py new file mode 100644 index 0000000..e69de29 From f20eba708b8b153ca0433eea1c03b06a4b88f1bd Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:43:32 +0100 Subject: [PATCH 10/14] feat: wire up neccesary modules in main --- app/main.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 1ec9153..ba14e5c 100644 --- a/app/main.py +++ b/app/main.py @@ -2,13 +2,33 @@ from fastapi import FastAPI +from app.api.v1.router import api_router +from app.core.config import settings +from app.core.exception_handlers import register_exception_handlers +from app.core.logger import get_logger, setup_logging +from app.db.redis import close_redis + +setup_logging() + +logging = get_logger(__name__) + @asynccontextmanager async def lifespan(app: FastAPI): + logging.info("Starting up the application...") yield + logging.info("Shutting down the application...") + await close_redis() + +app = FastAPI( + title=settings.APP_NAME, + version="0.1.0", + lifespan=lifespan, +) -app = FastAPI(version="0.1.0", lifespan=lifespan) +register_exception_handlers(app) +app.include_router(api_router, prefix=settings.API_V1_PREFIX) @app.get("/") From e6521bb4ab1bb8d4917bbe5ddeef37c8c9f358c4 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 12:43:50 +0100 Subject: [PATCH 11/14] chore: ignore spec file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 505a3b1..8e230db 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ wheels/ # Virtual environments .venv +/docs/SPEC.md From 4b949397da8b2cf3415a3e25ca2a2497e55d5811 Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 14:07:43 +0100 Subject: [PATCH 12/14] chore: task runner with uv --- pyproject.toml | 8 ++++++ uv.lock | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index cdce820..d7cba67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,4 +31,12 @@ dev = [ "httpx>=0.28.1", "httpx2>=2.3.0", "ruff>=0.15.16", + "taskipy>=1.14.1", ] + +[tool.taskipy.tasks] +dev = "uvicorn app.main:app --reload " +migrate = "alembic upgrade head" +test = "pytest tests/" +lint = "ruff check ." +format = "ruff format ." diff --git a/uv.lock b/uv.lock index 80ae3b9..8c7960a 100644 --- a/uv.lock +++ b/uv.lock @@ -288,6 +288,7 @@ dev = [ { name = "httpx" }, { name = "httpx2" }, { name = "ruff" }, + { name = "taskipy" }, ] [package.metadata] @@ -307,6 +308,7 @@ dev = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "httpx2", specifier = ">=2.3.0" }, { name = "ruff", specifier = ">=0.15.16" }, + { name = "taskipy", specifier = ">=1.14.1" }, ] [[package]] @@ -587,6 +589,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mslex" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583, upload-time = "2024-10-16T13:16:18.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820, upload-time = "2024-10-16T13:16:17.566Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -605,6 +616,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "psutil" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -965,6 +991,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, ] +[[package]] +name = "taskipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "mslex", marker = "sys_platform == 'win32'" }, + { name = "psutil" }, + { name = "tomli", marker = "python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/44/572261df3db9c6c3332f8618fafeb07a578fd18b06673c73f000f3586749/taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed", size = 14475, upload-time = "2024-11-26T16:37:46.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/97/4e4cfb1391c81e926bebe3d68d5231b5dbc3bb41c6ba48349e68a881462d/taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1", size = 13052, upload-time = "2024-11-26T16:37:44.546Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "truststore" version = "0.10.4" From 549c1510cfab659054a3fdc485e3a28da4874c1d Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 14:12:13 +0100 Subject: [PATCH 13/14] chore: add missing package --- pyproject.toml | 1 + uv.lock | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d7cba67..4c5a382 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "pydantic-settings>=2.14.1", "pytest>=9.0.3", "sqlalchemy[asyncio]>=2.0.50", + "structlog>=26.1.0", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index 8c7960a..9a203d4 100644 --- a/uv.lock +++ b/uv.lock @@ -281,6 +281,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pytest" }, { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "structlog" }, ] [package.dev-dependencies] @@ -301,6 +302,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.14.1" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.50" }, + { name = "structlog", specifier = ">=26.1.0" }, ] [package.metadata.requires-dev] @@ -991,6 +993,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, ] +[[package]] +name = "structlog" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/89/b4a0bcfdf4f71a3dea31379f095929613d7e4528a0996bca6aa964cd0dca/structlog-26.1.0.tar.gz", hash = "sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7", size = 1459881, upload-time = "2026-06-06T07:33:39.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" }, +] + [[package]] name = "taskipy" version = "1.14.1" From 386ee9d8bc7fbced9f3b1148d38ec640116911ff Mon Sep 17 00:00:00 2001 From: Muizzyranking Date: Thu, 11 Jun 2026 14:18:04 +0100 Subject: [PATCH 14/14] chore: add redis --- pyproject.toml | 1 + uv.lock | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4c5a382..784aa60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "httpx2>=2.3.0", "pydantic-settings>=2.14.1", "pytest>=9.0.3", + "redis>=8.0.0", "sqlalchemy[asyncio]>=2.0.50", "structlog>=26.1.0", ] diff --git a/uv.lock b/uv.lock index 9a203d4..3b07718 100644 --- a/uv.lock +++ b/uv.lock @@ -280,6 +280,7 @@ dependencies = [ { name = "httpx2" }, { name = "pydantic-settings" }, { name = "pytest" }, + { name = "redis" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "structlog" }, ] @@ -301,6 +302,7 @@ requires-dist = [ { name = "httpx2", specifier = ">=2.3.0" }, { name = "pydantic-settings", specifier = ">=2.14.1" }, { name = "pytest", specifier = ">=9.0.3" }, + { name = "redis", specifier = ">=8.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.50" }, { name = "structlog", specifier = ">=26.1.0" }, ] @@ -815,6 +817,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "redis" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ae/ed461cca5780b5fc8b9fe8ca0ed98d89508645fb9d880c24cc42c087678f/redis-8.0.0.tar.gz", hash = "sha256:a00c5355432051ac14e593b8b197fc76c887ee12d55a0984f69328a1115fdc49", size = 5101591, upload-time = "2026-05-28T12:45:13.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" }, +] + [[package]] name = "rich" version = "15.0.0"