From 546ee498ef3b284c3cbeea793040159005e583b5 Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Thu, 23 Oct 2025 12:35:00 +0200 Subject: [PATCH 1/7] added rate limiter --- src/config.py | 8 ++++++++ src/main.py | 7 +++++-- src/middleware/rate_limiter_middleware.py | 24 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/middleware/rate_limiter_middleware.py diff --git a/src/config.py b/src/config.py index 1234e137..e714f2c7 100644 --- a/src/config.py +++ b/src/config.py @@ -3,6 +3,7 @@ from pydantic import Field from dotenv import load_dotenv from pydantic_settings import BaseSettings +import time class Config(BaseSettings): @@ -69,5 +70,12 @@ class Config(BaseSettings): PROFILE: bool = False LOGGER: bool = True + ##ratelimiter settings + + REQUEST_COUNTER: int = 0 + LAST_REQUEST_TIME: float = time.time() + RATE_LIMIT_WINDOW: int = 60 + MAX_REQUESTS_PER_WINDOW: int = 100 + config = Config() diff --git a/src/main.py b/src/main.py index 41b0fd33..e1a81511 100644 --- a/src/main.py +++ b/src/main.py @@ -25,7 +25,7 @@ from fastapi.middleware.cors import CORSMiddleware from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor # type: ignore - +from src.middleware.rate_limiter_middleware import RateLimiterMiddleware from src.middleware.exception_handling_middleware import ExceptionFilterMiddleware from src.logger import DOT_API_LOGGER_NAME, get_dot_api_logger @@ -68,13 +68,16 @@ async def lifespan(app: FastAPI): allow_methods=["*"], # Allow all HTTP methods allow_headers=["*"], # Allow all HTTP headers ) -app.add_middleware(ExceptionFilterMiddleware) if config.PROFILE: # this will generate a profile.html at repository root when running any endpoint app.add_middleware(PyInstrumentMiddleWare) +app.add_middleware(RateLimiterMiddleware) +app.add_middleware(ExceptionFilterMiddleware) + + @app.get("/", status_code=status.HTTP_200_OK) async def root(): return {"message": "Welcome to the DOT api"} diff --git a/src/middleware/rate_limiter_middleware.py b/src/middleware/rate_limiter_middleware.py new file mode 100644 index 00000000..a458d223 --- /dev/null +++ b/src/middleware/rate_limiter_middleware.py @@ -0,0 +1,24 @@ +import time +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware +from src.config import config +from fastapi import status, HTTPException + + +class RateLimiterMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): # type: ignore + + current_time = time.time() + if current_time - config.LAST_REQUEST_TIME > config.RATE_LIMIT_WINDOW: + config.REQUEST_COUNTER = 0 + config.LAST_REQUEST_TIME = current_time + + config.REQUEST_COUNTER += 1 + + if config.REQUEST_COUNTER > config.MAX_REQUESTS_PER_WINDOW: + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail="Rate limit exceeded", + ) + response = await call_next(request) + return response From 26b8cec1eedd8e84cd2ac86ee152a997bfb9c856 Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Thu, 23 Oct 2025 12:39:41 +0200 Subject: [PATCH 2/7] added rate limiter --- src/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.py b/src/config.py index e714f2c7..c592b5f2 100644 --- a/src/config.py +++ b/src/config.py @@ -74,7 +74,7 @@ class Config(BaseSettings): REQUEST_COUNTER: int = 0 LAST_REQUEST_TIME: float = time.time() - RATE_LIMIT_WINDOW: int = 60 + RATE_LIMIT_WINDOW: int = 60 # in seconds MAX_REQUESTS_PER_WINDOW: int = 100 From f1518fa72579086b7f898c6f6544e7e76c882e47 Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Thu, 23 Oct 2025 14:39:20 +0200 Subject: [PATCH 3/7] added client ip --- src/config.py | 4 ---- src/middleware/rate_limiter_middleware.py | 26 ++++++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/config.py b/src/config.py index c592b5f2..25ccc3b7 100644 --- a/src/config.py +++ b/src/config.py @@ -3,7 +3,6 @@ from pydantic import Field from dotenv import load_dotenv from pydantic_settings import BaseSettings -import time class Config(BaseSettings): @@ -71,9 +70,6 @@ class Config(BaseSettings): LOGGER: bool = True ##ratelimiter settings - - REQUEST_COUNTER: int = 0 - LAST_REQUEST_TIME: float = time.time() RATE_LIMIT_WINDOW: int = 60 # in seconds MAX_REQUESTS_PER_WINDOW: int = 100 diff --git a/src/middleware/rate_limiter_middleware.py b/src/middleware/rate_limiter_middleware.py index a458d223..daf53645 100644 --- a/src/middleware/rate_limiter_middleware.py +++ b/src/middleware/rate_limiter_middleware.py @@ -3,22 +3,38 @@ from starlette.middleware.base import BaseHTTPMiddleware from src.config import config from fastapi import status, HTTPException +from typing import Dict + +rate_limit_mapper: Dict[str, int] = {} +last_request_time = time.time() + + +async def get_client_ip(request: Request) -> str: + return request.client.host # type: ignore class RateLimiterMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # type: ignore + global last_request_time, rate_limit_mapper + client_ip = await get_client_ip(request) + if not client_ip: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Client IP missing", + ) current_time = time.time() - if current_time - config.LAST_REQUEST_TIME > config.RATE_LIMIT_WINDOW: - config.REQUEST_COUNTER = 0 - config.LAST_REQUEST_TIME = current_time + if current_time - last_request_time > config.RATE_LIMIT_WINDOW: + rate_limit_mapper = {} + last_request_time = current_time - config.REQUEST_COUNTER += 1 + rate_limit_mapper[client_ip] = rate_limit_mapper.get(client_ip, 0) + 1 - if config.REQUEST_COUNTER > config.MAX_REQUESTS_PER_WINDOW: + if rate_limit_mapper[client_ip] > config.MAX_REQUESTS_PER_WINDOW: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Rate limit exceeded", ) + response = await call_next(request) return response From 64e3c07fffc028ae918178271caadbfaadfed20a Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Thu, 23 Oct 2025 15:12:03 +0200 Subject: [PATCH 4/7] added session to track the api limit --- src/middleware/rate_limiter_middleware.py | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/middleware/rate_limiter_middleware.py b/src/middleware/rate_limiter_middleware.py index daf53645..3b9238d5 100644 --- a/src/middleware/rate_limiter_middleware.py +++ b/src/middleware/rate_limiter_middleware.py @@ -4,33 +4,40 @@ from src.config import config from fastapi import status, HTTPException from typing import Dict +from fastapi.responses import JSONResponse +import uuid -rate_limit_mapper: Dict[str, int] = {} -last_request_time = time.time() +request_counters: Dict[str, int] = {} +last_request_times: Dict[str, float] = {} -async def get_client_ip(request: Request) -> str: - return request.client.host # type: ignore +async def create_session_id(response: JSONResponse) -> str: + """Generate and set a new session ID.""" + session_id = str(uuid.uuid4()) + response.set_cookie(key="session_id", value=session_id, secure=True) + return session_id class RateLimiterMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # type: ignore - global last_request_time, rate_limit_mapper - client_ip = await get_client_ip(request) - if not client_ip: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Client IP missing", - ) + session_id = request.cookies.get("session_id") + if not session_id: + response = JSONResponse(content={"message": "New session created."}) + session_id = await create_session_id(response) + response.headers["X-New-Session-ID"] = session_id current_time = time.time() - if current_time - last_request_time > config.RATE_LIMIT_WINDOW: - rate_limit_mapper = {} - last_request_time = current_time - rate_limit_mapper[client_ip] = rate_limit_mapper.get(client_ip, 0) + 1 + if ( + session_id not in last_request_times + or current_time - last_request_times[session_id] > config.RATE_LIMIT_WINDOW + ): + request_counters[session_id] = 0 + last_request_times[session_id] = current_time + + request_counters[session_id] += 1 - if rate_limit_mapper[client_ip] > config.MAX_REQUESTS_PER_WINDOW: + if request_counters[session_id] > config.MAX_REQUESTS_PER_WINDOW: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Rate limit exceeded", From 89275ce1db8a7dc988f797b5dbe6ca4151af5d46 Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Fri, 24 Oct 2025 09:46:24 +0200 Subject: [PATCH 5/7] added httponly property --- src/middleware/rate_limiter_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/rate_limiter_middleware.py b/src/middleware/rate_limiter_middleware.py index 3b9238d5..ac4490e5 100644 --- a/src/middleware/rate_limiter_middleware.py +++ b/src/middleware/rate_limiter_middleware.py @@ -14,7 +14,7 @@ async def create_session_id(response: JSONResponse) -> str: """Generate and set a new session ID.""" session_id = str(uuid.uuid4()) - response.set_cookie(key="session_id", value=session_id, secure=True) + response.set_cookie(key="session_id", value=session_id, secure=True, httponly=True) return session_id From 97799d8ccb98e47f59bd822032482f99cd372c40 Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Thu, 19 Feb 2026 10:09:02 +0100 Subject: [PATCH 6/7] added rate limiter with slow api --- poetry.lock | 240 ++++++++++++++++-- pyproject.toml | 1 + src/config.py | 2 +- src/main.py | 9 +- .../exception_handling_middleware.py | 13 + src/middleware/rate_limiter_middleware.py | 53 +--- tests/test_rate_limiter.py | 17 ++ 7 files changed, 264 insertions(+), 71 deletions(-) create mode 100644 tests/test_rate_limiter.py diff --git a/poetry.lock b/poetry.lock index 62c550c6..325c8591 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -17,6 +18,7 @@ version = "3.13.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, @@ -150,7 +152,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli (>=1.2)", "aiodns (>=3.3.0)", "backports.zstd", "brotlicffi (>=1.2)"] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] name = "aiomysql" @@ -158,6 +160,7 @@ version = "0.3.2" description = "MySQL driver for asyncio." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2"}, {file = "aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a"}, @@ -176,6 +179,7 @@ version = "0.5.0" description = "ODBC driver for asyncio." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aioodbc-0.5.0-py3-none-any.whl", hash = "sha256:bcaf16f007855fa4bf0ce6754b1f72c6c5a3d544188849577ddd55c5dc42985e"}, {file = "aioodbc-0.5.0.tar.gz", hash = "sha256:cbccd89ce595c033a49c9e6b4b55bbace7613a104b8a46e3d4c58c4bc4f25075"}, @@ -190,6 +194,7 @@ version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, @@ -205,6 +210,7 @@ version = "0.21.0" description = "asyncio bridge to the standard sqlite3 module" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"}, {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"}, @@ -223,6 +229,7 @@ version = "1.18.3" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "alembic-1.18.3-py3-none-any.whl", hash = "sha256:12a0359bfc068a4ecbb9b3b02cf77856033abfdb59e4a5aca08b7eacd7b74ddd"}, {file = "alembic-1.18.3.tar.gz", hash = "sha256:1212aa3778626f2b0f0aa6dd4e99a5f99b94bd25a0c1ac0bba3be65e081e50b0"}, @@ -242,6 +249,7 @@ version = "0.0.4" description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, @@ -253,6 +261,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -264,6 +273,7 @@ version = "4.12.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, @@ -274,7 +284,7 @@ idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"] +trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] [[package]] name = "asgi-lifespan" @@ -282,6 +292,7 @@ version = "2.1.0" description = "Programmatic startup/shutdown of ASGI apps." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308"}, {file = "asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f"}, @@ -296,6 +307,7 @@ version = "3.11.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, @@ -310,6 +322,7 @@ version = "2.1.0" description = "Simple LRU cache for asyncio" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e"}, {file = "async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed"}, @@ -321,6 +334,7 @@ version = "25.4.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, @@ -332,6 +346,7 @@ version = "1.6.6" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"}, {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"}, @@ -346,6 +361,7 @@ version = "1.38.0" description = "Microsoft Azure Core Library for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335"}, {file = "azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993"}, @@ -365,6 +381,7 @@ version = "1.0.0b12" description = "Microsoft Azure Azure Core OpenTelemetry plugin Library for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "azure_core_tracing_opentelemetry-1.0.0b12-py3-none-any.whl", hash = "sha256:38fd42709f1cc4bbc4f2797008b1c30a6a01617e49910c05daa3a0d0c65053ac"}, {file = "azure_core_tracing_opentelemetry-1.0.0b12.tar.gz", hash = "sha256:bb454142440bae11fd9d68c7c1d67ae38a1756ce808c5e4d736730a7b4b04144"}, @@ -380,6 +397,7 @@ version = "1.25.1" description = "Microsoft Azure Identity Library for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651"}, {file = "azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456"}, @@ -398,6 +416,7 @@ version = "1.8.6" description = "Microsoft Azure Monitor Opentelemetry Distro Client Library for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "azure_monitor_opentelemetry-1.8.6-py3-none-any.whl", hash = "sha256:2323eeba15bd2f016806e15f5bd6d24ddd62cc06beae0a06ab3fcfab38d3d120"}, {file = "azure_monitor_opentelemetry-1.8.6.tar.gz", hash = "sha256:8301c377f2c0550dc9b87b273b746d5841ef8e5117b6c542c9a6f7f3c7f34c20"}, @@ -423,6 +442,7 @@ version = "1.0.0b47" description = "Microsoft Azure Monitor Opentelemetry Exporter Client Library for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "azure_monitor_opentelemetry_exporter-1.0.0b47-py2.py3-none-any.whl", hash = "sha256:be1eca7ddfc07436793981313a68662e14713902f7e7fa7cf81736f1cf6d8bf8"}, {file = "azure_monitor_opentelemetry_exporter-1.0.0b47.tar.gz", hash = "sha256:c1207bd1c356aa77255e256f1af8eb2ac40a3bf51f90735f456056def7ac38c0"}, @@ -442,6 +462,7 @@ version = "25.12.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, @@ -492,6 +513,7 @@ version = "2026.1.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, @@ -503,6 +525,8 @@ version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -599,6 +623,7 @@ version = "3.4.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, @@ -721,6 +746,7 @@ version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, @@ -735,6 +761,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -746,6 +774,7 @@ version = "1.3.3" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, @@ -837,6 +866,7 @@ version = "7.13.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0"}, {file = "coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b"}, @@ -933,7 +963,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" @@ -941,6 +971,7 @@ version = "46.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] files = [ {file = "cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485"}, {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc"}, @@ -994,7 +1025,7 @@ files = [ ] [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] @@ -1012,6 +1043,7 @@ version = "0.12.1" description = "Composable style cycles" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -1021,12 +1053,31 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "deprecated" +version = "1.3.1" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"}, + {file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"}, +] + +[package.dependencies] +wrapt = ">=1.10,<3" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] + [[package]] name = "fastapi" version = "0.121.3" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.121.3-py3-none-any.whl", hash = "sha256:0c78fc87587fcd910ca1bbf5bc8ba37b80e119b388a7206b39f0ecc95ebf53e9"}, {file = "fastapi-0.121.3.tar.gz", hash = "sha256:0055bc24fe53e56a40e9e0ad1ae2baa81622c406e548e501e717634e2dfbc40b"}, @@ -1049,6 +1100,7 @@ version = "7.3.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, @@ -1065,6 +1117,7 @@ version = "4.61.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24"}, {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958"}, @@ -1119,17 +1172,17 @@ files = [ ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.45.0)"] symfont = ["sympy"] -type1 = ["xattr"] -unicode = ["unicodedata2 (>=17.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +type1 = ["xattr ; sys_platform == \"darwin\""] +unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] [[package]] name = "frozenlist" @@ -1137,6 +1190,7 @@ version = "1.8.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, @@ -1276,6 +1330,7 @@ version = "3.3.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, @@ -1342,6 +1397,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1353,6 +1409,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1374,6 +1431,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1386,7 +1444,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1398,6 +1456,7 @@ version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, @@ -1412,6 +1471,7 @@ version = "8.7.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, @@ -1421,13 +1481,13 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] [[package]] name = "iniconfig" @@ -1435,6 +1495,7 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -1446,6 +1507,7 @@ version = "0.7.2" description = "An ISO 8601 date/time/duration parser and formatter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, @@ -1457,6 +1519,7 @@ version = "1.5.3" description = "Lightweight pipelining with Python functions" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"}, {file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"}, @@ -1468,6 +1531,7 @@ version = "1.4.0" description = "JSON Web Token library for Python 3." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "jwt-1.4.0-py3-none-any.whl", hash = "sha256:7560a7f1de4f90de94ac645ee0303ac60c95b9e08e058fb69f6c330f71d71b11"}, {file = "jwt-1.4.0.tar.gz", hash = "sha256:f6f789128ac247142c79ee10f3dba6e366ec4e77c9920d18c1592e28aa0a7952"}, @@ -1486,6 +1550,7 @@ version = "1.4.9" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, @@ -1590,12 +1655,41 @@ files = [ {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, ] +[[package]] +name = "limits" +version = "5.8.0" +description = "Rate limiting utilities" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "limits-5.8.0-py3-none-any.whl", hash = "sha256:ae1b008a43eb43073c3c579398bd4eb4c795de60952532dc24720ab45e1ac6b8"}, + {file = "limits-5.8.0.tar.gz", hash = "sha256:c9e0d74aed837e8f6f50d1fcebcf5fd8130957287206bc3799adaee5092655da"}, +] + +[package.dependencies] +deprecated = ">=1.2" +packaging = ">=21" +typing-extensions = "*" + +[package.extras] +async-memcached = ["memcachio (>=0.3)"] +async-mongodb = ["motor (>=3,<4)"] +async-redis = ["coredis (>=3.4.0,<6)"] +async-valkey = ["valkey (>=6)"] +memcached = ["pymemcache (>3,<5.0.0)"] +mongodb = ["pymongo (>4.1,<5)"] +redis = ["redis (>3,!=4.5.2,!=4.5.3,<8.0.0)"] +rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] +valkey = ["valkey (>=6)"] + [[package]] name = "mako" version = "1.3.10" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, @@ -1615,6 +1709,7 @@ version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -1713,6 +1808,7 @@ version = "3.10.8" description = "Python plotting package" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7"}, {file = "matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656"}, @@ -1791,6 +1887,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1802,6 +1899,7 @@ version = "1.34.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1"}, {file = "msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f"}, @@ -1813,7 +1911,7 @@ PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" [package.extras] -broker = ["pymsalruntime (>=0.14,<0.19)", "pymsalruntime (>=0.17,<0.19)", "pymsalruntime (>=0.18,<0.19)"] +broker = ["pymsalruntime (>=0.14,<0.19) ; python_version >= \"3.6\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.19) ; python_version >= \"3.8\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.18,<0.19) ; python_version >= \"3.8\" and platform_system == \"Linux\""] [[package]] name = "msal-extensions" @@ -1821,6 +1919,7 @@ version = "1.3.1" description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, @@ -1838,6 +1937,7 @@ version = "0.7.1" description = "AutoRest swagger generator Python client runtime." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, @@ -1851,7 +1951,7 @@ requests = ">=2.16,<3.0" requests-oauthlib = ">=0.5.0" [package.extras] -async = ["aiodns", "aiohttp (>=3.0)"] +async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version >= \"3.5\""] [[package]] name = "multidict" @@ -1859,6 +1959,7 @@ version = "6.7.1" description = "multidict implementation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, @@ -2014,6 +2115,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -2025,6 +2127,7 @@ version = "3.6" description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"}, {file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"}, @@ -2047,6 +2150,7 @@ version = "2.4.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"}, {file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"}, @@ -2128,6 +2232,7 @@ version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, @@ -2144,6 +2249,7 @@ version = "0.10.0" description = "An OData query parser and transpiler." optional = false python-versions = "<4.0,>=3.7" +groups = ["main"] files = [ {file = "odata_query-0.10.0-py3-none-any.whl", hash = "sha256:eeb9061e87fb047c40bc3e59c5f1fbf5dbf4ea0f7ff2fd510d365d6c33fe7fcb"}, {file = "odata_query-0.10.0.tar.gz", hash = "sha256:230d847d334fc36dc072164886711ad794bedec6d864c2600de614c596fc1c4f"}, @@ -2167,6 +2273,7 @@ version = "1.39.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459"}, {file = "opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9"}, @@ -2182,6 +2289,7 @@ version = "0.60b0" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a"}, {file = "opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74"}, @@ -2199,6 +2307,7 @@ version = "0.60b0" description = "ASGI instrumentation for OpenTelemetry" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3"}, {file = "opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91"}, @@ -2220,6 +2329,7 @@ version = "0.60b0" description = "OpenTelemetry Database API instrumentation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_dbapi-0.60b0-py3-none-any.whl", hash = "sha256:429d8ca34a44a4296b9b09a1bd373fff350998d200525c6e79883c3328559b03"}, {file = "opentelemetry_instrumentation_dbapi-0.60b0.tar.gz", hash = "sha256:2b7eb38e46890cebe5bc1a1c03d2ab07fc159b0b7b91342941ee33dd73876d84"}, @@ -2237,6 +2347,7 @@ version = "0.60b0" description = "OpenTelemetry Instrumentation for Django" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_django-0.60b0-py3-none-any.whl", hash = "sha256:95495649c8c34ce9217c6873cdd10fc4fcaa67c25f8329adc54f5b286999e40b"}, {file = "opentelemetry_instrumentation_django-0.60b0.tar.gz", hash = "sha256:461e6fca27936ba97eec26da38bb5f19310783370478c7ca3a3e40faaceac9cc"}, @@ -2259,6 +2370,7 @@ version = "0.60b0" description = "OpenTelemetry FastAPI Instrumentation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee"}, {file = "opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3"}, @@ -2280,6 +2392,7 @@ version = "0.60b0" description = "Flask instrumentation for OpenTelemetry" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_flask-0.60b0-py3-none-any.whl", hash = "sha256:106e5774f79ac9b86dd0d949c1b8f46c807a8af16184301e10d24fc94e680d04"}, {file = "opentelemetry_instrumentation_flask-0.60b0.tar.gz", hash = "sha256:560f08598ef40cdcf7ca05bfb2e3ea74fab076e676f4c18bb36bb379bf5c4a1b"}, @@ -2302,6 +2415,7 @@ version = "0.60b0" description = "OpenTelemetry psycopg2 instrumentation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_psycopg2-0.60b0-py3-none-any.whl", hash = "sha256:ea136a32babd559aa717c04dddf6aa78aa94b816fb4e10dfe06751727ef306d4"}, {file = "opentelemetry_instrumentation_psycopg2-0.60b0.tar.gz", hash = "sha256:59e527fd97739440380634ffcf9431aa7f2965d939d8d5829790886e2b54ede9"}, @@ -2321,6 +2435,7 @@ version = "0.60b0" description = "OpenTelemetry requests instrumentation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_requests-0.60b0-py3-none-any.whl", hash = "sha256:e9957f3a650ae55502fa227b29ff985b37d63e41c85e6e1555d48039f092ea83"}, {file = "opentelemetry_instrumentation_requests-0.60b0.tar.gz", hash = "sha256:5079ed8df96d01dab915a0766cd28a49be7c33439ce43d6d39843ed6dee3204f"}, @@ -2341,6 +2456,7 @@ version = "0.60b0" description = "OpenTelemetry urllib instrumentation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_urllib-0.60b0-py3-none-any.whl", hash = "sha256:80e3545d02505dc0ea61b3a0a141ec2828e11bee6b7dedfd3ee7ed9a7adbf862"}, {file = "opentelemetry_instrumentation_urllib-0.60b0.tar.gz", hash = "sha256:89b8796f9ab64d0ea0833cfea98745963baa0d7e4a775b3d2a77791aa97cf3f9"}, @@ -2358,6 +2474,7 @@ version = "0.60b0" description = "OpenTelemetry urllib3 instrumentation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_urllib3-0.60b0-py3-none-any.whl", hash = "sha256:9a07504560feae650a9205b3e2a579a835819bb1d55498d26a5db477fe04bba0"}, {file = "opentelemetry_instrumentation_urllib3-0.60b0.tar.gz", hash = "sha256:6ae1640a993901bae8eda5496d8b1440fb326a29e4ba1db342738b8868174aad"}, @@ -2379,6 +2496,7 @@ version = "0.60b0" description = "WSGI Middleware for OpenTelemetry" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_instrumentation_wsgi-0.60b0-py3-none-any.whl", hash = "sha256:0ff80614c1e73f7e94a5860c7e6222a51195eebab3dc5f50d89013db3d5d2f13"}, {file = "opentelemetry_instrumentation_wsgi-0.60b0.tar.gz", hash = "sha256:5815195b1b9890f55c4baafec94ff98591579a7d9b16256064adea8ee5784651"}, @@ -2396,6 +2514,7 @@ version = "0.1.5" description = "Azure Resource Detector for OpenTelemetry" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "opentelemetry_resource_detector_azure-0.1.5-py3-none-any.whl", hash = "sha256:4dcc5d54ab5c3b11226af39509bc98979a8b9e0f8a24c1b888783755d3bf00eb"}, {file = "opentelemetry_resource_detector_azure-0.1.5.tar.gz", hash = "sha256:e0ba658a87c69eebc806e75398cd0e9f68a8898ea62de99bc1b7083136403710"}, @@ -2410,6 +2529,7 @@ version = "1.39.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514"}, {file = "opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde"}, @@ -2426,6 +2546,7 @@ version = "0.60b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a"}, {file = "opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f"}, @@ -2441,6 +2562,7 @@ version = "0.60b0" description = "Web util for OpenTelemetry" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe"}, {file = "opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d"}, @@ -2452,6 +2574,7 @@ version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, @@ -2463,6 +2586,7 @@ version = "3.0.0" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850"}, {file = "pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2"}, @@ -2554,6 +2678,7 @@ version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, @@ -2571,6 +2696,7 @@ version = "12.1.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"}, {file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"}, @@ -2679,6 +2805,7 @@ version = "4.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, @@ -2695,6 +2822,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -2710,6 +2838,7 @@ version = "0.4.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, @@ -2841,6 +2970,7 @@ version = "7.2.2" description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, @@ -2866,8 +2996,8 @@ files = [ ] [package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel", "wmi"] -test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32", "setuptools", "wheel", "wmi"] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] [[package]] name = "pyagrum" @@ -2875,6 +3005,7 @@ version = "2.3.2" description = "Bayesian networks and other Probabilistic Graphical Models." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "pyagrum-2.3.2-cp310-abi3-macosx_10_15_x86_64.whl", hash = "sha256:4ff6f445c4a4ca4b044ef68d4eaa43e9cf8a96e9715c706404cbad880c5761b4"}, {file = "pyagrum-2.3.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eb28aa11cb690a91ba1260774f9fc9432dc3afdf0502c62a05e7958dcc35ad9f"}, @@ -2895,6 +3026,7 @@ version = "2.14.0" description = "Python style guide checker" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, @@ -2906,6 +3038,8 @@ version = "3.0" description = "C parser in Python" optional = false python-versions = ">=3.10" +groups = ["main"] +markers = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, @@ -2917,6 +3051,7 @@ version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, @@ -2930,7 +3065,7 @@ typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -2938,6 +3073,7 @@ version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, @@ -3071,6 +3207,7 @@ version = "2.12.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"}, {file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"}, @@ -3094,6 +3231,7 @@ version = "4.0.1" description = "Python interface to Graphviz's Dot" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6"}, {file = "pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5"}, @@ -3115,6 +3253,7 @@ version = "3.4.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, @@ -3126,6 +3265,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -3140,6 +3280,7 @@ version = "5.1.2" description = "Call stack profiler for Python. Shows you why your code is slow!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyinstrument-5.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f224fe80ba288a00980af298d3808219f9d246fd95b4f91729c9c33a0dc54fe6"}, {file = "pyinstrument-5.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7df09fc0d5b72daf48b73cdf07738761bff7f656c81aff686b3ccdd7d2abe236"}, @@ -3221,6 +3362,7 @@ version = "2.11.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, @@ -3241,6 +3383,7 @@ version = "1.1.2" description = "Pure Python MySQL Driver" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9"}, {file = "pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03"}, @@ -3256,6 +3399,7 @@ version = "5.3.0" description = "DB API module for ODBC" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyodbc-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5"}, {file = "pyodbc-5.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2"}, @@ -3329,6 +3473,7 @@ version = "3.3.2" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, @@ -3343,6 +3488,7 @@ version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, @@ -3364,6 +3510,7 @@ version = "1.3.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, @@ -3383,6 +3530,7 @@ version = "6.3.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749"}, {file = "pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2"}, @@ -3402,6 +3550,7 @@ version = "0.6" description = "pytest plugin to run your tests in a specific order" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytest-ordering-0.6.tar.gz", hash = "sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6"}, {file = "pytest_ordering-0.6-py2-none-any.whl", hash = "sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2"}, @@ -3417,6 +3566,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -3431,6 +3581,7 @@ version = "1.2.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, @@ -3445,6 +3596,7 @@ version = "0.4.1" description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, @@ -3499,6 +3651,7 @@ version = "2.32.5" description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -3520,6 +3673,7 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" +groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -3538,6 +3692,7 @@ version = "1.8.0" description = "A set of python modules for machine learning and data mining" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da"}, {file = "scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1"}, @@ -3599,6 +3754,7 @@ version = "1.17.0" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd"}, {file = "scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558"}, @@ -3669,7 +3825,7 @@ numpy = ">=1.26.4,<2.7" [package.extras] dev = ["click (<8.3.0)", "cython-lint (>=0.12.2)", "mypy (==1.10.0)", "pycodestyle", "ruff (>=0.12.0)", "spin", "types-psutil", "typing_extensions"] doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)", "tabulate"] -test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest (>=8.0.0)", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest (>=8.0.0)", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "six" @@ -3677,17 +3833,37 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "slowapi" +version = "0.1.9" +description = "A rate limiting extension for Starlette and Fastapi" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["main"] +files = [ + {file = "slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36"}, + {file = "slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77"}, +] + +[package.dependencies] +limits = ">=2.3" + +[package.extras] +redis = ["redis (>=3.4.1,<4.0.0)"] + [[package]] name = "sly" version = "0.4" description = "SLY - Sly Lex Yacc" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "sly-0.4.tar.gz", hash = "sha256:e5f2266a231322cc17519fbc3a3ba1c6335fed5a9a55abe0e598a35aea0ac32a"}, ] @@ -3701,6 +3877,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3712,6 +3889,7 @@ version = "2.0.46" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735"}, {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39"}, @@ -3809,6 +3987,7 @@ version = "0.50.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca"}, {file = "starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"}, @@ -3827,6 +4006,7 @@ version = "3.6.0" description = "threadpoolctl" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, @@ -3838,6 +4018,7 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, @@ -3849,6 +4030,7 @@ version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, @@ -3863,6 +4045,8 @@ version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\"" files = [ {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, @@ -3874,16 +4058,17 @@ version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] -brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" @@ -3891,6 +4076,7 @@ version = "0.35.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, @@ -3901,7 +4087,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "wrapt" @@ -3909,6 +4095,7 @@ version = "1.17.3" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, @@ -3999,6 +4186,7 @@ version = "2025.12.0" description = "N-D labeled arrays and datasets in Python" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "xarray-2025.12.0-py3-none-any.whl", hash = "sha256:9e77e820474dbbe4c6c2954d0da6342aa484e33adaa96ab916b15a786181e970"}, {file = "xarray-2025.12.0.tar.gz", hash = "sha256:73f6a6fadccc69c4d45bdd70821a47c72de078a8a0313ff8b1e97cd54ac59fed"}, @@ -4024,6 +4212,7 @@ version = "1.22.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, @@ -4168,13 +4357,14 @@ version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] @@ -4182,6 +4372,6 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_it type = ["pytest-mypy"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "5f3723c12f52c9c3149019e254cc50a28b1e4a42adbf4ca40ad85829aed88530" +content-hash = "00ea33d4e576476cb16b63a71d5e8d043a04cbfd783c6511edb7e533e65985a7" diff --git a/pyproject.toml b/pyproject.toml index f552e873..5456ecda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ azure-monitor-opentelemetry = "^1.8.1" azure-monitor-opentelemetry-exporter = "^1.0.0b44" xarray = "^2025.10.1" pytest-ordering = "^0.6" +slowapi = "^0.1.9" [tool.pytest.ini_options] asyncio_mode = "strict" diff --git a/src/config.py b/src/config.py index 0d1a49ff..c49b61a7 100644 --- a/src/config.py +++ b/src/config.py @@ -73,7 +73,7 @@ class Config(BaseSettings): ##ratelimiter settings RATE_LIMIT_WINDOW: int = 60 # in seconds - MAX_REQUESTS_PER_WINDOW: int = 100 + MAX_REQUESTS_PER_WINDOW: int = 100 # max requests per window config = Config() diff --git a/src/main.py b/src/main.py index abb156b4..62921a6b 100644 --- a/src/main.py +++ b/src/main.py @@ -27,7 +27,9 @@ from fastapi.middleware.cors import CORSMiddleware from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor # type: ignore -from src.middleware.rate_limiter_middleware import RateLimiterMiddleware +from src.middleware.rate_limiter_middleware import limiter +from slowapi.middleware import SlowAPIMiddleware + from src.middleware.exception_handling_middleware import ExceptionFilterMiddleware from src.logger import DOT_API_LOGGER_NAME, get_dot_api_logger @@ -51,7 +53,8 @@ async def lifespan(app: FastAPI): swagger_ui_parameters={"syntaxHighlight": False}, lifespan=lifespan, ) - +app.state.limiter = limiter +app.add_middleware(SlowAPIMiddleware) if config.LOGGER: try: configure_azure_monitor( @@ -75,8 +78,6 @@ async def lifespan(app: FastAPI): # this will generate a profile.html at repository root when running any endpoint app.add_middleware(PyInstrumentMiddleWare) - -app.add_middleware(RateLimiterMiddleware) app.add_middleware(ExceptionFilterMiddleware) diff --git a/src/middleware/exception_handling_middleware.py b/src/middleware/exception_handling_middleware.py index 2e4ea6cc..43251424 100644 --- a/src/middleware/exception_handling_middleware.py +++ b/src/middleware/exception_handling_middleware.py @@ -1,5 +1,6 @@ from fastapi import HTTPException, Request from fastapi.responses import JSONResponse +from slowapi.errors import RateLimitExceeded from src.logger import get_dot_api_logger from starlette.middleware.base import BaseHTTPMiddleware from fastapi.exceptions import RequestValidationError @@ -15,6 +16,18 @@ async def dispatch(self, request: Request, call_next): # type: ignore # Process request and response response = await call_next(request) return response + except RateLimitExceeded as exc: + # Handle rate limit exceeded + logger.warning( + f"Rate limit exceeded for {request.client.host if request.client else 'unknown'}" + ) + return JSONResponse( + status_code=429, + content={ + "detail": "Rate limit exceeded. Please try again later.", + "retry_after": exc.detail, + }, + ) except HTTPException as exc: # Log and return custom message for HTTP exceptions logger.error(f"HTTPException: {exc}") diff --git a/src/middleware/rate_limiter_middleware.py b/src/middleware/rate_limiter_middleware.py index ac4490e5..857507a0 100644 --- a/src/middleware/rate_limiter_middleware.py +++ b/src/middleware/rate_limiter_middleware.py @@ -1,47 +1,18 @@ -import time from fastapi import Request -from starlette.middleware.base import BaseHTTPMiddleware +from slowapi import Limiter +from slowapi.util import get_remote_address from src.config import config -from fastapi import status, HTTPException -from typing import Dict -from fastapi.responses import JSONResponse -import uuid -request_counters: Dict[str, int] = {} -last_request_times: Dict[str, float] = {} +def get_client_ip(request: Request) -> str: + """Extract client IP, considering X-Forwarded-For for proxied requests.""" + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + return get_remote_address(request) or "unknown" -async def create_session_id(response: JSONResponse) -> str: - """Generate and set a new session ID.""" - session_id = str(uuid.uuid4()) - response.set_cookie(key="session_id", value=session_id, secure=True, httponly=True) - return session_id - -class RateLimiterMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next): # type: ignore - session_id = request.cookies.get("session_id") - - if not session_id: - response = JSONResponse(content={"message": "New session created."}) - session_id = await create_session_id(response) - response.headers["X-New-Session-ID"] = session_id - current_time = time.time() - - if ( - session_id not in last_request_times - or current_time - last_request_times[session_id] > config.RATE_LIMIT_WINDOW - ): - request_counters[session_id] = 0 - last_request_times[session_id] = current_time - - request_counters[session_id] += 1 - - if request_counters[session_id] > config.MAX_REQUESTS_PER_WINDOW: - raise HTTPException( - status_code=status.HTTP_429_TOO_MANY_REQUESTS, - detail="Rate limit exceeded", - ) - - response = await call_next(request) - return response +# Default rate limit string (e.g., "100/minute") +DEFAULT_RATE_LIMIT = f"{config.MAX_REQUESTS_PER_WINDOW}/{config.RATE_LIMIT_WINDOW}second" +# Create limiter instance with client IP as the key +limiter = Limiter(key_func=get_client_ip, default_limits=[DEFAULT_RATE_LIMIT]) diff --git a/tests/test_rate_limiter.py b/tests/test_rate_limiter.py new file mode 100644 index 00000000..fe9dac0a --- /dev/null +++ b/tests/test_rate_limiter.py @@ -0,0 +1,17 @@ +import pytest +from httpx import AsyncClient + +from src.config import config + + +@pytest.mark.asyncio +async def test_rate_limiter_with_root_endpoint(client: AsyncClient): + """Test rate limiting on the root endpoint.""" + # Make requests up to the configured limit + max_requests = config.MAX_REQUESTS_PER_WINDOW + + # First batch of requests should succeed + for i in range(min(max_requests, 10)): # Test up to 10 to keep test fast + response = await client.get("/") + assert response.status_code == 200, f"Request {i+1} failed unexpectedly" + assert response.json() == {"message": "Welcome to the DOT api"} From 6a25adce1f4bbdcce02ab22ee5b0d4c557e0a083 Mon Sep 17 00:00:00 2001 From: rupakkatwal Date: Thu, 19 Feb 2026 11:01:59 +0100 Subject: [PATCH 7/7] err handling in client key --- src/config.py | 2 +- src/main.py | 7 ++++-- src/middleware/rate_limiter_middleware.py | 30 +++++++++++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/config.py b/src/config.py index c49b61a7..5fb01b04 100644 --- a/src/config.py +++ b/src/config.py @@ -73,7 +73,7 @@ class Config(BaseSettings): ##ratelimiter settings RATE_LIMIT_WINDOW: int = 60 # in seconds - MAX_REQUESTS_PER_WINDOW: int = 100 # max requests per window + MAX_REQUESTS_PER_WINDOW: int = 1000 # max requests per window config = Config() diff --git a/src/main.py b/src/main.py index 62921a6b..daa8bca0 100644 --- a/src/main.py +++ b/src/main.py @@ -53,8 +53,7 @@ async def lifespan(app: FastAPI): swagger_ui_parameters={"syntaxHighlight": False}, lifespan=lifespan, ) -app.state.limiter = limiter -app.add_middleware(SlowAPIMiddleware) + if config.LOGGER: try: configure_azure_monitor( @@ -65,6 +64,10 @@ async def lifespan(app: FastAPI): except Exception as e: logger.info("Error occurred while configuring telemetry: %s", e) +# add rate limiter middleware +app.state.limiter = limiter +app.add_middleware(SlowAPIMiddleware) + # Adding CORS middleware to the FastAPI application app.add_middleware( CORSMiddleware, diff --git a/src/middleware/rate_limiter_middleware.py b/src/middleware/rate_limiter_middleware.py index 857507a0..0291e6cb 100644 --- a/src/middleware/rate_limiter_middleware.py +++ b/src/middleware/rate_limiter_middleware.py @@ -1,18 +1,34 @@ -from fastapi import Request +from fastapi import HTTPException, Request from slowapi import Limiter from slowapi.util import get_remote_address from src.config import config -def get_client_ip(request: Request) -> str: - """Extract client IP, considering X-Forwarded-For for proxied requests.""" +def get_client_key(request: Request) -> str: + """ + Get rate limit key for the client. + + Priority: + 1. IP address from X-Forwarded-For header (for proxied requests) + 2. Direct client IP address(for local/development requests) + """ + # Try X-Forwarded-For first (for requests ) forwarded = request.headers.get("X-Forwarded-For") if forwarded: - return forwarded.split(",")[0].strip() - return get_remote_address(request) or "unknown" + ip = forwarded.split(",")[0].strip() + if ip: + return f"ip:{ip}" + + # Try direct client IP for local + ip = get_remote_address(request) + if ip: + return f"ip:{ip}" + + # Cannot identify client - reject request + raise HTTPException(status_code=403, detail="Unable to identify client for rate limiting") # Default rate limit string (e.g., "100/minute") DEFAULT_RATE_LIMIT = f"{config.MAX_REQUESTS_PER_WINDOW}/{config.RATE_LIMIT_WINDOW}second" -# Create limiter instance with client IP as the key -limiter = Limiter(key_func=get_client_ip, default_limits=[DEFAULT_RATE_LIMIT]) +# Create limiter instance with client key function +limiter = Limiter(key_func=get_client_key, default_limits=[DEFAULT_RATE_LIMIT])