diff --git a/AGENTS.md b/AGENTS.md index 209a659..fbd8a7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ ### Package Structure - `mt5api/`: Main FastAPI package. - - `main.py`: App wiring, lifespan handling, middleware, CORS, and router registration. + - `main.py`: App wiring, lifespan handling, middleware, and router registration. - `__main__.py`: CLI entry point for launching the API from environment-backed configuration. - `config.py`: Environment-backed configuration normalization and validation. - `auth.py`: Optional API key authentication helpers. @@ -68,7 +68,7 @@ ## Security & Configuration Tips - Do not commit real MT5 credentials or API keys. -- Configure `MT5API_SECRET_KEY`, `MT5API_RATE_LIMIT`, `MT5API_CORS_ORIGINS`, `MT5API_ROUTER_PREFIX`, and `MT5API_LOG_LEVEL` through environment variables. +- Configure `MT5API_SECRET_KEY`, `MT5API_ROUTER_PREFIX`, and `MT5API_LOG_LEVEL` through environment variables. - Keep runtime configuration in the environment instead of hardcoding deployment values. - Authentication mode is fixed at process startup; cover both authenticated and unauthenticated behavior when changing auth-related code. diff --git a/README.md b/README.md index 7bad65b..4974c23 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ MetaTrader 5 REST API mt5api exposes MT5 market data, account info, trading history, and trading operations over HTTP. It uses the [`pdmt5`](https://github.com/dceoy/pdmt5) -client internally and adds optional API-key auth, rate limiting, and -JSON/Parquet response formatting. +client internally and adds optional API-key auth and JSON/Parquet response +formatting. The API server must run on Windows. The `MetaTrader5` Python package used by `pdmt5` is supported only on Windows, so you must host `mt5api` on a Windows @@ -22,7 +22,7 @@ graph TB subgraph "Windows Host" subgraph "FastAPI Application" - Middleware["Middleware Stack
CORS · Logging · Error Handler · Rate Limiter"] + Middleware["Middleware Stack
Logging · Error Handler"] Routers["Routers
health · symbols · market · account · history · calc · trading"] Auth["API Key Security Dependency
Security(api_key_header) · verify_api_key"] Deps["FastAPI Dependencies
MT5 Client Singleton · Format Negotiation"] @@ -45,8 +45,8 @@ graph TB - REST endpoints for symbols, market data, account info, orders, history, calculations, and trading operations - JSON and Apache Parquet responses (content negotiation) -- Optional API key authentication with per-minute rate limiting -- Structured JSON logging and configurable CORS +- Optional API key authentication +- Structured JSON logging - OpenAPI/Swagger docs built into the API ## Requirements diff --git a/docs/api/deployment.md b/docs/api/deployment.md index f45b10b..ca1c149 100644 --- a/docs/api/deployment.md +++ b/docs/api/deployment.md @@ -36,9 +36,7 @@ nssm install mt5api # Optional: set MT5API_SECRET_KEY only when you want to require X-API-Key headers. MT5API_SECRET_KEY=your-secret-api-key MT5API_LOG_LEVEL=INFO -MT5API_RATE_LIMIT=100 MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS=100 -MT5API_CORS_ORIGINS=* MT5API_ROUTER_PREFIX=/api/v1 ``` diff --git a/docs/api/rest-api.md b/docs/api/rest-api.md index 256973e..4d9854b 100644 --- a/docs/api/rest-api.md +++ b/docs/api/rest-api.md @@ -30,9 +30,7 @@ Set the optional API key and other limits via environment variables: ```powershell $env:MT5API_SECRET_KEY = "your-secret-api-key" # Optional: omit to disable auth $env:MT5API_LOG_LEVEL = "INFO" -$env:MT5API_RATE_LIMIT = "100" $env:MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS = "100" -$env:MT5API_CORS_ORIGINS = "*" $env:MT5API_ROUTER_PREFIX = "/api/v1" # Optional: omit for root-level routes ``` @@ -70,10 +68,7 @@ disabled and those endpoints are accessible without authorization. curl -H "X-API-Key: your-secret-api-key" "http://windows-host:8000/symbols" ``` -## Rate Limiting - -Rate limiting uses `slowapi` with a default limit of `100/minute`. Set -`MT5API_RATE_LIMIT` to an integer for a different per-minute cap. +## Subscription Limits Active market-book subscriptions are capped at `100` symbols by default. Set `MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS` to a positive integer to adjust that @@ -121,9 +116,9 @@ If `MT5API_ROUTER_PREFIX` is configured, prepend it to each API route below. - `GET /rates/range` (`symbol`, `timeframe`, `date_from`, `date_to`, `format`) - `GET /ticks/from` (`symbol`, `date_from`, `count`, `flags`, `format`) - `GET /ticks/range` (`symbol`, `date_from`, `date_to`, `flags`, `format`) -- `GET /market-book/{symbol}` (`format`) — Market depth (DOM) -- `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events -- `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events +- `GET /market-book/{symbol}` (`format`) — Market depth (DOM) *(experimental)* +- `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events *(experimental)* +- `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events *(experimental)* ### Calculations @@ -315,9 +310,8 @@ Errors follow RFC 7807 Problem Details: Minimum security posture for deployments: - Set `MT5API_SECRET_KEY` to enable API key authentication when needed -- Rate limiting enabled (`MT5API_RATE_LIMIT`) +- Configure rate limiting at the reverse proxy or API gateway level - Run behind HTTPS in production -- Restrict CORS origins (`MT5API_CORS_ORIGINS`) for public deployments - Restrict access to operational endpoints (`/order/check`, `/symbols/{symbol}/select`, `/market-book/{symbol}/subscribe`, `/market-book/{symbol}/unsubscribe`) to trusted clients only diff --git a/docs/index.md b/docs/index.md index 158f4c3..0c2fd19 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ trading operations. mt5api exposes MT5 data and trading operations over HTTP using FastAPI. It relies on the underlying MT5 client library for connectivity and adds optional -authentication, rate limiting, and response formatting suitable for analytics +authentication and response formatting suitable for analytics workflows. The API server must run on Windows because the `MetaTrader5` Python package is @@ -19,8 +19,8 @@ logged in. API clients can connect from any operating system. - REST endpoints for symbols, market data, account info, orders, history, calculations, and trading operations - JSON and Apache Parquet responses -- Optional API key authentication and rate limiting -- Structured JSON logging and configurable CORS +- Optional API key authentication +- Structured JSON logging - OpenAPI/Swagger docs built in ## Requirements diff --git a/mt5api/config.py b/mt5api/config.py index 6b117fa..43660c7 100644 --- a/mt5api/config.py +++ b/mt5api/config.py @@ -6,18 +6,14 @@ import re from .constants import ( - DEFAULT_API_CORS_ORIGINS, DEFAULT_API_HOST, DEFAULT_API_LOG_LEVEL, - DEFAULT_API_RATE_LIMIT, DEFAULT_API_ROUTER_PREFIX, DEFAULT_MAX_MARKET_BOOK_SUBSCRIPTIONS, - ENV_MT5API_CORS_ORIGINS, ENV_MT5API_HOST, ENV_MT5API_LOG_LEVEL, ENV_MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS, ENV_MT5API_PORT, - ENV_MT5API_RATE_LIMIT, ENV_MT5API_ROUTER_PREFIX, ENV_MT5API_SECRET_KEY, ) @@ -81,24 +77,6 @@ def get_configured_api_log_level() -> str: return os.getenv(ENV_MT5API_LOG_LEVEL, DEFAULT_API_LOG_LEVEL) -def get_configured_api_rate_limit() -> str: - """Get the configured API rate limit string. - - Returns: - Raw per-minute rate-limit string from configuration. - """ - return os.getenv(ENV_MT5API_RATE_LIMIT, str(DEFAULT_API_RATE_LIMIT)) - - -def get_configured_api_cors_origins() -> str: - """Get the configured CORS origins string. - - Returns: - Raw CORS origins configuration string. - """ - return os.getenv(ENV_MT5API_CORS_ORIGINS, DEFAULT_API_CORS_ORIGINS) - - def get_configured_api_router_prefix() -> str: """Get the configured API router prefix. diff --git a/mt5api/constants.py b/mt5api/constants.py index 3a82768..e3b74a3 100644 --- a/mt5api/constants.py +++ b/mt5api/constants.py @@ -20,8 +20,6 @@ ENV_MT5API_HOST = "MT5API_HOST" ENV_MT5API_PORT = "MT5API_PORT" ENV_MT5API_LOG_LEVEL = "MT5API_LOG_LEVEL" -ENV_MT5API_RATE_LIMIT = "MT5API_RATE_LIMIT" -ENV_MT5API_CORS_ORIGINS = "MT5API_CORS_ORIGINS" ENV_MT5API_ROUTER_PREFIX = "MT5API_ROUTER_PREFIX" ENV_MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS = "MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS" ENV_MT5API_SECRET_KEY = "MT5API_SECRET_KEY" # noqa: S105 @@ -29,8 +27,6 @@ DEFAULT_API_HOST = "0.0.0.0" # noqa: S104 DEFAULT_API_PORT = 8000 DEFAULT_API_LOG_LEVEL = "INFO" -DEFAULT_API_RATE_LIMIT = 100 -DEFAULT_API_CORS_ORIGINS = "*" DEFAULT_API_ROUTER_PREFIX = "" DEFAULT_MAX_MARKET_BOOK_SUBSCRIPTIONS = 100 MAX_API_PORT = 65535 diff --git a/mt5api/main.py b/mt5api/main.py index a900987..199dcc9 100644 --- a/mt5api/main.py +++ b/mt5api/main.py @@ -10,11 +10,9 @@ from fastapi import FastAPI from fastapi.openapi.utils import get_openapi -from starlette.middleware.cors import CORSMiddleware from .auth import is_auth_enabled from .config import ( - get_configured_api_cors_origins, get_configured_api_log_level, get_configured_api_router_prefix, get_configured_max_market_book_subscriptions, @@ -28,7 +26,6 @@ API_REDOC_URL, API_TITLE, API_VERSION, - DEFAULT_API_CORS_ORIGINS, MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY, MAX_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY, ) @@ -70,19 +67,6 @@ def _configure_logging() -> None: root_logger.addHandler(handler) -def _get_cors_origins() -> list[str]: - """Get CORS origins from environment. - - Returns: - List of allowed origins. - """ - raw_origins = get_configured_api_cors_origins() - if raw_origins.strip() == DEFAULT_API_CORS_ORIGINS: - return [DEFAULT_API_CORS_ORIGINS] - - return [origin.strip() for origin in raw_origins.split(",") if origin.strip()] - - def _strip_auth_from_openapi(openapi_schema: dict[str, Any]) -> None: """Remove API key requirements from OpenAPI when auth is disabled.""" openapi_schema.pop("security", None) @@ -212,15 +196,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: ) app.openapi = _custom_openapi -# Add CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=_get_cors_origins(), - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - # Add middleware add_middleware(app) diff --git a/mt5api/middleware.py b/mt5api/middleware.py index 4016e72..3342684 100644 --- a/mt5api/middleware.py +++ b/mt5api/middleware.py @@ -1,4 +1,4 @@ -"""Error handling, logging, and rate limiting middleware.""" +"""Error handling and logging middleware.""" from __future__ import annotations @@ -10,13 +10,7 @@ from fastapi.responses import JSONResponse from pdmt5.mt5 import Mt5RuntimeError from pydantic import ValidationError -from slowapi import Limiter -from slowapi.errors import RateLimitExceeded -from slowapi.middleware import SlowAPIMiddleware -from slowapi.util import get_remote_address -from .config import get_configured_api_rate_limit -from .constants import DEFAULT_API_RATE_LIMIT from .models import ErrorResponse if TYPE_CHECKING: @@ -168,58 +162,11 @@ async def logging_middleware( return response -def _build_default_rate_limit() -> str: - """Build the default rate limit string from environment config. - - Returns: - Default rate limit string in slowapi format. - """ - raw_limit = get_configured_api_rate_limit() - - try: - limit_value = max(1, int(raw_limit)) - except ValueError: - limit_value = DEFAULT_API_RATE_LIMIT - - return f"{limit_value}/minute" - - def add_middleware(app: FastAPI) -> None: """Add middleware and error handlers to the FastAPI application. Args: app: FastAPI application instance. """ - # Configure rate limiting - limiter = Limiter( - key_func=get_remote_address, - default_limits=[_build_default_rate_limit()], - ) - app.state.limiter = limiter - app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) - app.add_middleware(SlowAPIMiddleware) - - # Add custom middleware app.middleware("http")(error_handler_middleware) app.middleware("http")(logging_middleware) - - -def _rate_limit_exceeded_handler( - request: Request, # noqa: ARG001 - exc: Exception, # noqa: ARG001 -) -> JSONResponse: - """Handle rate limiting errors. - - Returns: - JSON response describing the rate limit error. - """ - return JSONResponse( - status_code=status.HTTP_429_TOO_MANY_REQUESTS, - content={ - "type": "/errors/rate-limit", - "title": "Rate Limit Exceeded", - "status": status.HTTP_429_TOO_MANY_REQUESTS, - "detail": "Too many requests. Please slow down.", - "instance": None, - }, - ) diff --git a/mt5api/routers/market.py b/mt5api/routers/market.py index ccd01ed..1ac011b 100644 --- a/mt5api/routers/market.py +++ b/mt5api/routers/market.py @@ -167,8 +167,8 @@ async def get_ticks_range( @router.get( "/market-book/{symbol}", response_model=DataResponse, - summary="Get market book", - description="Get market depth (DOM) for a symbol", + summary="Get market book (experimental)", + description="**Experimental.** Get market depth (DOM) for a symbol", ) async def get_market_book( mt5_client: Annotated[Mt5DataClient, Depends(get_mt5_client)], diff --git a/mt5api/routers/trading.py b/mt5api/routers/trading.py index 83dcaf0..e2c5690 100644 --- a/mt5api/routers/trading.py +++ b/mt5api/routers/trading.py @@ -139,8 +139,8 @@ async def post_symbol_select( @router.post( "/market-book/{symbol}/subscribe", response_model=DataResponse, - summary="Subscribe to market depth", - description="Subscribe to Market Depth change events for a symbol", + summary="Subscribe to market depth (experimental)", + description="**Experimental.** Subscribe to Market Depth events for a symbol", ) async def post_market_book_subscribe( app_request: Request, @@ -188,8 +188,8 @@ async def post_market_book_subscribe( @router.post( "/market-book/{symbol}/unsubscribe", response_model=DataResponse, - summary="Unsubscribe from market depth", - description="Cancel Market Depth subscription for a symbol", + summary="Unsubscribe from market depth (experimental)", + description="**Experimental.** Cancel Market Depth subscription for a symbol", ) async def post_market_book_unsubscribe( app_request: Request, diff --git a/pyproject.toml b/pyproject.toml index 6041609..33405f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mt5api" -version = "0.0.4" +version = "0.1.0" description = "MetaTrader 5 REST API" authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}] maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}] @@ -17,7 +17,6 @@ dependencies = [ "python-multipart >= 0.0.9", "python-jose[cryptography] >= 3.3.0", "passlib[bcrypt] >= 1.7.4", - "slowapi >= 0.1.9", "httpx >= 0.27.0", ] classifiers = [ diff --git a/tests/test_config.py b/tests/test_config.py index e579c52..67ef770 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,40 +8,12 @@ from mt5api.constants import ( DEFAULT_MAX_MARKET_BOOK_SUBSCRIPTIONS, - ENV_MT5API_CORS_ORIGINS, ENV_MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS, - ENV_MT5API_RATE_LIMIT, ENV_MT5API_ROUTER_PREFIX, ENV_MT5API_SECRET_KEY, ) -def test_get_cors_origins_parses_list(monkeypatch: pytest.MonkeyPatch) -> None: - """CORS origins should split on commas and trim whitespace.""" - monkeypatch.setenv(ENV_MT5API_CORS_ORIGINS, "https://a.example, https://b.example") - - from mt5api import main # noqa: PLC0415 - - assert main._get_cors_origins() == [ # pyright: ignore[reportPrivateUsage] - "https://a.example", - "https://b.example", - ] - - -def test_build_default_rate_limit_handles_invalid_value( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Invalid rate limit values should default to 100/minute.""" - monkeypatch.setenv(ENV_MT5API_RATE_LIMIT, "not-a-number") - - from mt5api import middleware # noqa: PLC0415 - - assert ( - middleware._build_default_rate_limit() # pyright: ignore[reportPrivateUsage] - == "100/minute" - ) - - @pytest.mark.parametrize( ("raw_prefix", "expected_prefix"), [ diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 827fdca..dd0fc89 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -11,14 +11,11 @@ from pdmt5.mt5 import Mt5RuntimeError from pydantic import BaseModel -from mt5api.constants import ENV_MT5API_RATE_LIMIT from mt5api.middleware import _create_error_response, add_middleware if TYPE_CHECKING: from collections.abc import Callable - import pytest - def _create_app(handler: Callable[[], object]) -> FastAPI: app = FastAPI() @@ -148,24 +145,3 @@ def handler() -> dict[str, str]: response = client.get("/boom") assert "X-Process-Time" in response.headers - - -def test_rate_limiting_enforced(monkeypatch: pytest.MonkeyPatch) -> None: - """Test rate limiting returns 429 when limit exceeded.""" - monkeypatch.setenv(ENV_MT5API_RATE_LIMIT, "1") - - app = FastAPI() - add_middleware(app) - - def limited() -> dict[str, str]: - return {"status": "ok"} - - app.get("/limited")(limited) - - client = TestClient(app) - - first = client.get("/limited") - second = client.get("/limited") - - assert first.status_code == 200 - assert second.status_code == 429 diff --git a/uv.lock b/uv.lock index 6117c37..6fae079 100644 --- a/uv.lock +++ b/uv.lock @@ -366,18 +366,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, ] -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "ecdsa" version = "0.19.1" @@ -525,20 +513,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "limits" -version = "5.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "packaging" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/e5/c968d43a65128cd54fb685f257aafb90cd5e4e1c67d084a58f0e4cbed557/limits-5.6.0.tar.gz", hash = "sha256:807fac75755e73912e894fdd61e2838de574c5721876a19f7ab454ae1fffb4b5", size = 182984, upload-time = "2025-09-29T17:15:22.689Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/96/4fcd44aed47b8fcc457653b12915fcad192cd646510ef3f29fd216f4b0ab/limits-5.6.0-py3-none-any.whl", hash = "sha256:b585c2104274528536a5b68864ec3835602b3c4a802cd6aa0b07419798394021", size = 60604, upload-time = "2025-09-29T17:15:18.419Z" }, -] - [[package]] name = "markdown" version = "3.10.1" @@ -743,7 +717,7 @@ wheels = [ [[package]] name = "mt5api" -version = "0.0.4" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, @@ -754,7 +728,6 @@ dependencies = [ { name = "pyarrow" }, { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, - { name = "slowapi" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -785,7 +758,6 @@ requires-dist = [ { name = "pyarrow", specifier = ">=18.0.0" }, { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, { name = "python-multipart", specifier = ">=0.0.9" }, - { name = "slowapi", specifier = ">=0.1.9" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, ] @@ -1391,18 +1363,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "slowapi" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "limits" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, -] - [[package]] name = "starlette" version = "0.50.0" @@ -1720,60 +1680,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] - -[[package]] -name = "wrapt" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, - { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, - { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, - { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, - { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, - { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, - { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, - { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, - { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, - { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, - { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, - { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, - { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, -]