├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── asgi_correlation_id ├── __init__.py ├── context.py ├── extensions │ ├── __init__.py │ ├── celery.py │ └── sentry.py ├── log_filters.py ├── middleware.py └── py.typed ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── test_extension_celery.py ├── test_extension_sentry.py ├── test_log_filter.py └── test_middleware.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish package 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | jobs: 8 | build-and-publish-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: snok/.github/workflows/publish@main 12 | with: 13 | overwrite-repository: true 14 | repository-url: https://test.pypi.org/legacy/ 15 | token: ${{ secrets.TEST_PYPI_TOKEN }} 16 | build-and-publish: 17 | needs: build-and-publish-test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: snok/.github/workflows/publish@main 21 | with: 22 | token: ${{ secrets.PYPI_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | linting: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.12" 17 | - run: pip install pre-commit 18 | - uses: actions/cache@v3 19 | id: pre-commit-cache 20 | with: 21 | path: ~/.cache/pre-commit 22 | key: key-0 23 | - run: pre-commit run --all-files 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | python-version: [ "3.8.18", "3.9.19", "3.10.14", "3.11.9", "3.12.4" ] 31 | # Ideally we would test starlette versions, not FastAPI, 32 | # but our test suite currently uses FastAPI comprehensively, 33 | # so we would need to redo that first. 34 | fastapi-version: [ "0.109", "0.110", "0.111"] 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: actions/setup-python@v4 38 | with: 39 | python-version: "${{ matrix.python-version }}" 40 | - uses: actions/cache@v3 41 | id: poetry-cache 42 | with: 43 | path: ~/.local 44 | key: ${{ matrix.python-version }}-1 45 | - uses: snok/install-poetry@v1 46 | with: 47 | virtualenvs-create: false 48 | version: latest 49 | - uses: actions/cache@v3 50 | id: cache-venv 51 | with: 52 | path: .venv 53 | key: ${{ hashFiles('**/poetry.lock') }}-${{ matrix.python-version }}-1 54 | - run: | 55 | python -m venv .venv 56 | source .venv/bin/activate 57 | pip install -U pip 58 | poetry add fastapi==${{ matrix.fastapi-version }} 59 | poetry install --no-interaction --no-root 60 | if: steps.cache-venv.outputs.cache-hit != 'true' 61 | - run: | 62 | python -m venv .venv 63 | source .venv/bin/activate 64 | pip install importlib-metadata==4.13.0 65 | if: matrix.python-version == '3.7.14' 66 | - name: Run tests 67 | run: | 68 | source .venv/bin/activate 69 | coverage run -m pytest tests 70 | coverage xml 71 | coverage report 72 | - uses: codecov/codecov-action@v2 73 | with: 74 | file: ./coverage.xml 75 | fail_ci_if_error: true 76 | if: matrix.python-version == '3.10.6' 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/* 3 | env/ 4 | venv/ 5 | .venv/ 6 | build/ 7 | dist/ 8 | *.egg-info/ 9 | notes 10 | .pytest_cache 11 | .coverage 12 | htmlcov/ 13 | 14 | # celery 15 | celerybeat-* 16 | .sourcery.yaml 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.8.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://github.com/pycqa/isort 8 | rev: 5.13.2 9 | hooks: 10 | - id: isort 11 | 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v4.6.0 14 | hooks: 15 | - id: check-ast 16 | - id: check-merge-conflict 17 | - id: check-case-conflict 18 | - id: check-docstring-first 19 | - id: check-json 20 | - id: check-yaml 21 | - id: end-of-file-fixer 22 | - id: trailing-whitespace 23 | - id: mixed-line-ending 24 | - id: trailing-whitespace 25 | - id: double-quote-string-fixer 26 | 27 | - repo: https://github.com/pycqa/flake8 28 | rev: 7.1.1 29 | hooks: 30 | - id: flake8 31 | additional_dependencies: [ 32 | 'flake8-bugbear', 33 | 'flake8-comprehensions', 34 | 'flake8-print', 35 | 'flake8-mutable', 36 | 'flake8-simplify', 37 | 'flake8-pytest-style', 38 | 'flake8-printf-formatting', 39 | 'flake8-type-checking', 40 | ] 41 | 42 | - repo: https://github.com/sirosen/check-jsonschema 43 | rev: 0.29.2 44 | hooks: 45 | - id: check-github-actions 46 | - id: check-github-workflows 47 | 48 | - repo: https://github.com/asottile/pyupgrade 49 | rev: v3.17.0 50 | hooks: 51 | - id: pyupgrade 52 | args: [ "--py36-plus", "--py37-plus" ] 53 | 54 | - repo: https://github.com/pre-commit/mirrors-mypy 55 | rev: v1.11.2 56 | hooks: 57 | - id: mypy 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sondre Lillebø Gundersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi](https://img.shields.io/pypi/v/asgi-correlation-id)](https://pypi.org/project/asgi-correlation-id/) 2 | [![test](https://github.com/snok/asgi-correlation-id/actions/workflows/test.yml/badge.svg)](https://github.com/snok/asgi-correlation-id/actions/workflows/test.yml) 3 | [![codecov](https://codecov.io/gh/snok/asgi-correlation-id/branch/main/graph/badge.svg?token=1aXlWPm2gb)](https://codecov.io/gh/snok/asgi-correlation-id) 4 | 5 | # ASGI Correlation ID middleware 6 | 7 | Middleware for reading or generating correlation IDs for each incoming request. Correlation IDs can then be added to your 8 | logs, making it simple to retrieve all logs generated from a single HTTP request. 9 | 10 | When the middleware detects a correlation ID HTTP header in an incoming request, the ID is stored. If no header is 11 | found, a correlation ID is generated for the request instead. 12 | 13 | The middleware checks for the `X-Request-ID` header by default, but can be set to any key. 14 | `X-Correlation-ID` is also pretty commonly used. 15 | 16 | ## Example 17 | 18 | Once logging is configured, your output will go from this: 19 | 20 | ``` 21 | INFO ... project.views This is an info log 22 | WARNING ... project.models This is a warning log 23 | INFO ... project.views This is an info log 24 | INFO ... project.views This is an info log 25 | WARNING ... project.models This is a warning log 26 | WARNING ... project.models This is a warning log 27 | ``` 28 | 29 | to this: 30 | 31 | ```docker 32 | INFO ... [773fa6885] project.views This is an info log 33 | WARNING ... [773fa6885] project.models This is a warning log 34 | INFO ... [0d1c3919e] project.views This is an info log 35 | INFO ... [99d44111e] project.views This is an info log 36 | WARNING ... [0d1c3919e] project.models This is a warning log 37 | WARNING ... [99d44111e] project.models This is a warning log 38 | ``` 39 | 40 | Now we're actually able to see which logs are related. 41 | 42 | # Installation 43 | 44 | ``` 45 | pip install asgi-correlation-id 46 | ``` 47 | 48 | # Setup 49 | 50 | To set up the package, you need to add the middleware and configure logging. 51 | 52 | ## Adding the middleware 53 | 54 | The middleware can be added like this: 55 | 56 | ```python 57 | from fastapi import FastAPI 58 | 59 | from asgi_correlation_id import CorrelationIdMiddleware 60 | 61 | app = FastAPI() 62 | app.add_middleware(CorrelationIdMiddleware) 63 | ``` 64 | 65 | or any other way your framework allows. 66 | 67 | For [Starlette](https://github.com/encode/starlette) apps, just substitute `FastAPI` with `Starlette` in all examples. 68 | 69 | ## Configure logging 70 | 71 | This section assumes you have already started configuring logging in your project. If this is not the case, check out 72 | the section on [setting up logging from scratch](#setting-up-logging-from-scratch) instead. 73 | 74 | To set up logging of the correlation ID, you simply have to add the log-filter the package provides. 75 | 76 | If your current log-config looked like this: 77 | 78 | ```python 79 | LOGGING = { 80 | 'version': 1, 81 | 'disable_existing_loggers': False, 82 | 'formatters': { 83 | 'web': { 84 | 'class': 'logging.Formatter', 85 | 'datefmt': '%H:%M:%S', 86 | 'format': '%(levelname)s ... %(name)s %(message)s', 87 | }, 88 | }, 89 | 'handlers': { 90 | 'web': { 91 | 'class': 'logging.StreamHandler', 92 | 'formatter': 'web', 93 | }, 94 | }, 95 | 'loggers': { 96 | 'my_project': { 97 | 'handlers': ['web'], 98 | 'level': 'DEBUG', 99 | 'propagate': True, 100 | }, 101 | }, 102 | } 103 | ``` 104 | 105 | You simply have to add the filter, like this: 106 | 107 | ```diff 108 | LOGGING = { 109 | 'version': 1, 110 | 'disable_existing_loggers': False, 111 | + 'filters': { 112 | + 'correlation_id': { 113 | + '()': 'asgi_correlation_id.CorrelationIdFilter', 114 | + 'uuid_length': 32, 115 | + 'default_value': '-', 116 | + }, 117 | + }, 118 | 'formatters': { 119 | 'web': { 120 | 'class': 'logging.Formatter', 121 | 'datefmt': '%H:%M:%S', 122 | + 'format': '%(levelname)s ... [%(correlation_id)s] %(name)s %(message)s', 123 | }, 124 | }, 125 | 'handlers': { 126 | 'web': { 127 | 'class': 'logging.StreamHandler', 128 | + 'filters': ['correlation_id'], 129 | 'formatter': 'web', 130 | }, 131 | }, 132 | 'loggers': { 133 | 'my_project': { 134 | 'handlers': ['web'], 135 | 'level': 'DEBUG', 136 | 'propagate': True, 137 | }, 138 | }, 139 | } 140 | ``` 141 | 142 | If you're using a json log-formatter, just add `correlation-id: %(correlation_id)s` to your list of properties. 143 | 144 | ## Middleware configuration 145 | 146 | The middleware can be configured in a few ways, but there are no required arguments. 147 | 148 | ```python 149 | app.add_middleware( 150 | CorrelationIdMiddleware, 151 | header_name='X-Request-ID', 152 | update_request_header=True, 153 | generator=lambda: uuid4().hex, 154 | validator=is_valid_uuid4, 155 | transformer=lambda a: a, 156 | ) 157 | ``` 158 | 159 | Configurable middleware arguments include: 160 | 161 | **header_name** 162 | 163 | - Type: `str` 164 | - Default: `X-Request-ID` 165 | - Description: The header name decides which HTTP header value to read correlation IDs from. `X-Request-ID` and 166 | `X-Correlation-ID` are common choices. 167 | 168 | **update_request_header** 169 | 170 | - Type: `bool` 171 | - Default: `True` 172 | - Description: Whether to update incoming request's header value with the generated correlation ID. This is to support 173 | use cases where it's relied on the presence of the request header (like various tracing middlewares). 174 | 175 | **generator** 176 | 177 | - Type: `Callable[[], str]` 178 | - Default: `lambda: uuid4().hex` 179 | - Description: The generator function is responsible for generating new correlation IDs when no ID is received from an 180 | incoming request's headers. We use UUIDs by default, but if you prefer, you could use libraries 181 | like [nanoid](https://github.com/puyuan/py-nanoid) or your own custom function. 182 | 183 | **validator** 184 | 185 | - Type: `Callable[[str], bool]` 186 | - Default: `is_valid_uuid4` ( 187 | found [here](https://github.com/snok/asgi-correlation-id/blob/main/asgi_correlation_id/middleware.py#L17)) 188 | - Description: The validator function is used when reading incoming HTTP header values. By default, we discard non-UUID 189 | formatted header values, to enforce correlation ID uniqueness. If you prefer to allow any header value, you can set 190 | this setting to `None`, or pass your own validator. 191 | 192 | **transformer** 193 | 194 | - Type: `Callable[[str], str]` 195 | - Default: `lambda a: a` 196 | - Description: Most users won't need a transformer, and by default we do nothing. 197 | The argument was added for cases where users might want to alter incoming or generated ID values in some way. It 198 | provides a mechanism for transforming an incoming ID in a way you see fit. See the middleware code for more context. 199 | 200 | ## CORS 201 | 202 | If you are using cross-origin resource sharing ([CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)), e.g. 203 | you are making requests to an API from a frontend JavaScript code served from a different origin, you have to ensure 204 | two things: 205 | 206 | - permit correlation ID header in the incoming requests' HTTP headers so the value can be reused by the middleware, 207 | - add the correlation ID header to the allowlist in responses' HTTP headers so it can be accessed by the browser. 208 | 209 | This can be best accomplished by using a dedicated middleware for your framework of choice. Here are some examples. 210 | 211 | ### Starlette 212 | 213 | Docs: https://www.starlette.io/middleware/#corsmiddleware 214 | 215 | ```python 216 | from starlette.applications import Starlette 217 | from starlette.middleware import Middleware 218 | from starlette.middleware.cors import CORSMiddleware 219 | 220 | 221 | middleware = [ 222 | Middleware( 223 | CORSMiddleware, 224 | allow_origins=['*'], 225 | allow_methods=['*'], 226 | allow_headers=['X-Requested-With', 'X-Request-ID'], 227 | expose_headers=['X-Request-ID'] 228 | ) 229 | ] 230 | 231 | app = Starlette(..., middleware=middleware) 232 | ``` 233 | 234 | ### FastAPI 235 | 236 | Docs: https://fastapi.tiangolo.com/tutorial/cors/ 237 | 238 | ```python 239 | from app.main import app 240 | from fastapi.middleware.cors import CORSMiddleware 241 | 242 | 243 | app.add_middleware( 244 | CORSMiddleware, 245 | allow_origins=['*'], 246 | allow_methods=['*'], 247 | allow_headers=['X-Requested-With', 'X-Request-ID'], 248 | expose_headers=['X-Request-ID'] 249 | ) 250 | ``` 251 | 252 | For more details on the topic, refer to the [CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol). 253 | 254 | ## Exception handling 255 | 256 | By default, the `X-Request-ID` response header will be included in all responses from the server, *except* in the case 257 | of unhandled server errors. If you wish to include request IDs in the case of a `500` error you can add a custom 258 | exception handler. 259 | 260 | Here are some simple examples to help you get started. See each framework's documentation for more info. 261 | 262 | ### Starlette 263 | 264 | Docs: https://www.starlette.io/exceptions/ 265 | 266 | ```python 267 | from starlette.requests import Request 268 | from starlette.responses import PlainTextResponse 269 | from starlette.applications import Starlette 270 | 271 | from asgi_correlation_id import correlation_id 272 | 273 | 274 | async def custom_exception_handler(request: Request, exc: Exception) -> PlainTextResponse: 275 | return PlainTextResponse( 276 | "Internal Server Error", 277 | status_code=500, 278 | headers={'X-Request-ID': correlation_id.get() or ""} 279 | ) 280 | 281 | 282 | app = Starlette( 283 | ..., 284 | exception_handlers={500: custom_exception_handler} 285 | ) 286 | ``` 287 | 288 | ### FastAPI 289 | 290 | Docs: https://fastapi.tiangolo.com/tutorial/handling-errors/ 291 | 292 | ```python 293 | from app.main import app 294 | from fastapi import HTTPException, Request 295 | from fastapi.exception_handlers import http_exception_handler 296 | from fastapi.responses import JSONResponse 297 | 298 | from asgi_correlation_id import correlation_id 299 | 300 | 301 | @app.exception_handler(Exception) 302 | async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: 303 | return await http_exception_handler( 304 | request, 305 | HTTPException( 306 | 500, 307 | 'Internal server error', 308 | headers={'X-Request-ID': correlation_id.get() or ""} 309 | )) 310 | ``` 311 | 312 | If you are using CORS, you also have to include the `Access-Control-Allow-Origin` and `Access-Control-Expose-Headers` 313 | headers in the error response. For more details, see the [CORS section](#cors) above. 314 | 315 | # Setting up logging from scratch 316 | 317 | If your project does not have logging configured, this section will explain how to get started. If you want even more 318 | details, take a look 319 | at [this blogpost](https://medium.com/@sondrelg_12432/setting-up-request-id-logging-for-your-fastapi-application-4dc190aac0ea) 320 | . 321 | 322 | The Python [docs](https://docs.python.org/3/library/logging.config.html) explain there are a few configuration functions 323 | you may use for simpler setup. For this example we will use `dictConfig`, because that's what, e.g., Django users should 324 | find most familiar, but the different configuration methods are interchangable, so if you want to use another method, 325 | just browse the python docs and change the configuration method as you please. 326 | 327 | The benefit of `dictConfig` is that it lets you specify your entire logging configuration in a single data structure, 328 | and it lets you add conditional logic to it. The following example shows how to set up both console and JSON logging: 329 | 330 | ```python 331 | from logging.config import dictConfig 332 | 333 | from app.core.config import settings 334 | 335 | 336 | def configure_logging() -> None: 337 | dictConfig( 338 | { 339 | 'version': 1, 340 | 'disable_existing_loggers': False, 341 | 'filters': { # correlation ID filter must be added here to make the %(correlation_id)s formatter work 342 | 'correlation_id': { 343 | '()': 'asgi_correlation_id.CorrelationIdFilter', 344 | 'uuid_length': 8 if not settings.ENVIRONMENT == 'local' else 32, 345 | 'default_value': '-', 346 | }, 347 | }, 348 | 'formatters': { 349 | 'console': { 350 | 'class': 'logging.Formatter', 351 | 'datefmt': '%H:%M:%S', 352 | # formatter decides how our console logs look, and what info is included. 353 | # adding %(correlation_id)s to this format is what make correlation IDs appear in our logs 354 | 'format': '%(levelname)s:\t\b%(asctime)s %(name)s:%(lineno)d [%(correlation_id)s] %(message)s', 355 | }, 356 | }, 357 | 'handlers': { 358 | 'console': { 359 | 'class': 'logging.StreamHandler', 360 | # Filter must be declared in the handler, otherwise it won't be included 361 | 'filters': ['correlation_id'], 362 | 'formatter': 'console', 363 | }, 364 | }, 365 | # Loggers can be specified to set the log-level to log, and which handlers to use 366 | 'loggers': { 367 | # project logger 368 | 'app': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': True}, 369 | # third-party package loggers 370 | 'databases': {'handlers': ['console'], 'level': 'WARNING'}, 371 | 'httpx': {'handlers': ['console'], 'level': 'INFO'}, 372 | 'asgi_correlation_id': {'handlers': ['console'], 'level': 'WARNING'}, 373 | }, 374 | } 375 | ) 376 | ``` 377 | 378 | With the logging configuration defined within a function like this, all you have to do is make sure to run the function 379 | on startup somehow, and logging should work for you. You can do this any way you'd like, but passing it to 380 | the `FastAPI.on_startup` list of callables is a good starting point. 381 | 382 | # Integration with structlog 383 | 384 | [structlog](https://www.structlog.org/) is a Python library that enables structured logging. 385 | 386 | It is trivial to configure with `asgi_correlation_id`: 387 | 388 | ```python 389 | import logging 390 | from typing import Any 391 | 392 | import structlog 393 | from asgi_correlation_id import correlation_id 394 | 395 | 396 | def add_correlation( 397 | logger: logging.Logger, method_name: str, event_dict: dict[str, Any] 398 | ) -> dict[str, Any]: 399 | """Add request id to log message.""" 400 | if request_id := correlation_id.get(): 401 | event_dict["request_id"] = request_id 402 | return event_dict 403 | 404 | 405 | structlog.configure( 406 | processors=[ 407 | add_correlation, 408 | structlog.stdlib.filter_by_level, 409 | structlog.stdlib.add_logger_name, 410 | structlog.stdlib.add_log_level, 411 | structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M.%S"), 412 | structlog.processors.StackInfoRenderer(), 413 | structlog.processors.format_exc_info, 414 | structlog.processors.JSONRenderer(), 415 | ], 416 | wrapper_class=structlog.stdlib.BoundLogger, 417 | logger_factory=structlog.stdlib.LoggerFactory(), 418 | cache_logger_on_first_use=True, 419 | ) 420 | ``` 421 | 422 | # Integration with [SAQ](https://github.com/tobymao/saq) 423 | 424 | If you're using [saq](https://github.com/tobymao/saq/), you 425 | can easily transfer request IDs from the web server to your 426 | workers by using the event hooks provided by the library: 427 | 428 | ```python 429 | from uuid import uuid4 430 | 431 | from asgi_correlation_id import correlation_id 432 | from saq import Job, Queue 433 | 434 | 435 | CID_TRANSFER_KEY = 'correlation_id' 436 | 437 | 438 | async def before_enqueue(job: Job) -> None: 439 | """ 440 | Transfer the correlation ID from the current context to the worker. 441 | 442 | This might be called from a web server or a worker process. 443 | """ 444 | job.meta[CID_TRANSFER_KEY] = correlation_id.get() or uuid4() 445 | 446 | 447 | async def before_process(ctx: dict) -> None: 448 | """ 449 | Load correlation ID from the enqueueing process to this one. 450 | """ 451 | correlation_id.set(ctx['job'].meta.get(CID_TRANSFER_KEY, uuid4())) 452 | 453 | 454 | async def after_process(ctx: dict) -> None: 455 | """ 456 | Reset correlation ID for this process. 457 | """ 458 | correlation_id.set(None) 459 | 460 | queue = Queue(...) 461 | queue.register_before_enqueue(before_enqueue) 462 | 463 | priority_settings = { 464 | ..., 465 | 'queue': queue, 466 | 'before_process': before_process, 467 | 'after_process': after_process, 468 | } 469 | ``` 470 | 471 | # Integration with [hypercorn](https://github.com/pgjones/hypercorn) 472 | To add a correlation ID to your [hypercorn](https://github.com/pgjones/hypercorn) logs, you'll need to add a log filter and change the log formatting. Here's an example of how to configure hypercorn, if you're running a [FastAPI](https://fastapi.tiangolo.com/deployment/manually/) app: 473 | 474 | ```python 475 | import logging 476 | import os 477 | 478 | from fastapi import APIRouter, FastAPI 479 | from hypercorn.config import Config 480 | from hypercorn.asyncio import serve 481 | import asgi_correlation_id 482 | import asyncio 483 | import hypercorn 484 | 485 | 486 | def configure_logging(): 487 | console_handler = logging.StreamHandler() 488 | console_handler.addFilter(asgi_correlation_id.CorrelationIdFilter()) 489 | logging.basicConfig( 490 | handlers=[console_handler], 491 | level="INFO", 492 | format="%(levelname)s log [%(correlation_id)s] %(name)s %(message)s") 493 | 494 | 495 | app = FastAPI(on_startup=[configure_logging]) 496 | app.add_middleware(asgi_correlation_id.CorrelationIdMiddleware) 497 | router = APIRouter() 498 | 499 | 500 | @router.get("/test") 501 | async def test_get(): 502 | print("toto") 503 | logger = logging.getLogger() 504 | logger.info("test_get") 505 | 506 | 507 | app.include_router(router) 508 | 509 | 510 | if __name__ == "__main__": 511 | logConfig = { 512 | "handlers": { 513 | "hypercorn.access": { 514 | "formatter": "hypercorn.access", 515 | "level": "INFO", 516 | "class": "logging.StreamHandler", 517 | "stream": "ext://sys.stdout", 518 | "filters": [ 519 | asgi_correlation_id.CorrelationIdFilter() 520 | ], 521 | }}, 522 | "formatters": { 523 | "hypercorn.access": { 524 | "format": "%(message)s %(correlation_id)s", 525 | } 526 | }, 527 | "loggers": { 528 | "hypercorn.access": { 529 | "handlers": [ 530 | "hypercorn.access" 531 | ], 532 | "level": "INFO", 533 | }, 534 | }, 535 | "version": 1 536 | } 537 | 538 | config = Config() 539 | # write access log to stdout 540 | config.accesslog = "-" 541 | 542 | config.logconfig_dict = logConfig 543 | asyncio.run(serve(app, config)) 544 | ``` 545 | 546 | ``` 547 | # run it 548 | $ python3 test.py 549 | 550 | # test it: 551 | $ curl http://localhost:8080/test 552 | 553 | # log on stdout: 554 | INFO log [7e7ccfff352a428991920d1da2502674] root test_get 555 | 127.0.0.1:34754 - - [14/Dec/2023:10:34:08 +0100] "GET /test 1.1" 200 4 "-" "curl/7.76.1" 7e7ccfff352a428991920d1da2502674 556 | ``` 557 | 558 | # Integration with [Uvicorn](https://github.com/encode/uvicorn) 559 | To add a correlation ID to your [uvicorn](https://github.com/encode/uvicorn) logs, you'll need to add a log filter and change the log formatting. Here's an example of how to configure uvicorn, if you're running a [FastAPI](https://fastapi.tiangolo.com/deployment/manually/) app: 560 | 561 | ```python 562 | import logging 563 | import os 564 | 565 | import asgi_correlation_id 566 | import uvicorn 567 | from fastapi import APIRouter, FastAPI 568 | from uvicorn.config import LOGGING_CONFIG 569 | 570 | 571 | def configure_logging(): 572 | console_handler = logging.StreamHandler() 573 | console_handler.addFilter(asgi_correlation_id.CorrelationIdFilter()) 574 | logging.basicConfig( 575 | handlers=[console_handler], 576 | level="INFO", 577 | format="%(levelname)s log [%(correlation_id)s] %(name)s %(message)s") 578 | 579 | 580 | app = FastAPI(on_startup=[configure_logging]) 581 | app.add_middleware(asgi_correlation_id.CorrelationIdMiddleware) 582 | router = APIRouter() 583 | 584 | 585 | @router.get("/test") 586 | async def test_get(): 587 | logger = logging.getLogger() 588 | logger.info("test_get") 589 | 590 | 591 | app.include_router(router) 592 | 593 | 594 | if __name__ == "__main__": 595 | LOGGING_CONFIG["handlers"]["access"]["filters"] = [asgi_correlation_id.CorrelationIdFilter()] 596 | LOGGING_CONFIG["formatters"]["access"]["fmt"] = "%(levelname)s access [%(correlation_id)s] %(name)s %(message)s" 597 | uvicorn.run("test:app", port=8080, log_level=os.environ.get("LOGLEVEL", "DEBUG").lower()) 598 | ``` 599 | 600 | ``` 601 | # run it 602 | python test.py 603 | 604 | # test it 605 | curl http://localhost:8080/test 606 | 607 | # log on stdout 608 | INFO log [16b61d57f9ff4a85ac80f5cd406e0aa2] root test_get 609 | INFO access [16b61d57f9ff4a85ac80f5cd406e0aa2] uvicorn.access 127.0.0.1:24810 - "GET /test HTTP/1.1" 200 610 | ``` 611 | 612 | # Extensions 613 | 614 | In addition to the middleware, we've added a couple of extensions for third-party packages. 615 | 616 | ## Sentry 617 | 618 | If your project has [sentry-sdk](https://pypi.org/project/sentry-sdk/) 619 | installed, correlation IDs will automatically be added to Sentry events as a `transaction_id`. 620 | 621 | See 622 | this [blogpost](https://blog.sentry.io/2019/04/04/trace-errors-through-stack-using-unique-identifiers-in-sentry#1-generate-a-unique-identifier-and-set-as-a-sentry-tag-on-issuing-service) 623 | for a little bit of detail. The transaction ID is displayed in the event detail view in Sentry and is just an easy way 624 | to connect logs to a Sentry event. 625 | 626 | ## Celery 627 | 628 | > Note: If you're using the celery integration, install the package with `pip install asgi-correlation-id[celery]` 629 | 630 | For Celery user's there's one primary issue: workers run as completely separate processes, so correlation IDs are lost 631 | when spawning background tasks from requests. 632 | 633 | However, with some Celery signal magic, we can actually transfer correlation IDs to worker processes, like this: 634 | 635 | ```python 636 | @before_task_publish.connect() 637 | def transfer_correlation_id(headers) -> None: 638 | # This is called before task.delay() finishes 639 | # Here we're able to transfer the correlation ID via the headers kept in our backend 640 | headers[header_key] = correlation_id.get() 641 | 642 | 643 | @task_prerun.connect() 644 | def load_correlation_id(task) -> None: 645 | # This is called when the worker picks up the task 646 | # Here we're able to load the correlation ID from the headers 647 | id_value = task.request.get(header_key) 648 | correlation_id.set(id_value) 649 | ``` 650 | 651 | To configure correlation ID transfer, simply import and run the setup function the package provides: 652 | 653 | ```python 654 | from asgi_correlation_id.extensions.celery import load_correlation_ids 655 | 656 | load_correlation_ids() 657 | ``` 658 | 659 | ### Taking it one step further - Adding Celery tracing IDs 660 | 661 | In addition to transferring request IDs to Celery workers, we've added one more log filter for improving tracing in 662 | celery processes. This is completely separate from correlation ID functionality, but is something we use ourselves, so 663 | keep in the package with the rest of the signals. 664 | 665 | The log filter adds an ID, `celery_current_id` for each worker process, and an ID, `celery_parent_id` for the process 666 | that spawned it. 667 | 668 | Here's a quick summary of outputs from different scenarios: 669 | 670 | | Scenario | Correlation ID | Celery Current ID | Celery Parent ID | 671 | |------------------------------------------ |--------------------|-------------------|------------------| 672 | | Request | ✅ | | | 673 | | Request -> Worker | ✅ | ✅ | | 674 | | Request -> Worker -> Another worker | ✅ | ✅ | ✅ | 675 | | Beat -> Worker | ✅* | ✅ | | | 676 | | Beat -> Worker -> Worker | ✅* | ✅ | ✅ | ✅ | 677 | 678 | *When we're in a process spawned separately from an HTTP request, a correlation ID is still spawned for the first 679 | process in the chain, and passed down. You can think of the correlation ID as an origin ID, while the combination of 680 | current and parent-ids as a way of linking the chain. 681 | 682 | To add the current and parent IDs, just alter your `celery.py` to this: 683 | 684 | ```diff 685 | + from asgi_correlation_id.extensions.celery import load_correlation_ids, load_celery_current_and_parent_ids 686 | 687 | load_correlation_ids() 688 | + load_celery_current_and_parent_ids() 689 | ``` 690 | 691 | If you wish to correlate celery task IDs through the IDs found in your broker (i.e., the celery `task_id`), use the `use_internal_celery_task_id` argument on `load_celery_current_and_parent_ids` 692 | ```diff 693 | from asgi_correlation_id.extensions.celery import load_correlation_ids, load_celery_current_and_parent_ids 694 | 695 | load_correlation_ids() 696 | + load_celery_current_and_parent_ids(use_internal_celery_task_id=True) 697 | ``` 698 | Note: `load_celery_current_and_parent_ids` will ignore the `generator` argument when `use_internal_celery_task_id` is set to `True` 699 | 700 | To set up the additional log filters, update your log config like this: 701 | 702 | ```diff 703 | LOGGING = { 704 | 'version': 1, 705 | 'disable_existing_loggers': False, 706 | 'filters': { 707 | 'correlation_id': { 708 | + '()': 'asgi_correlation_id.CorrelationIdFilter', 709 | + 'uuid_length': 32, 710 | + 'default_value': '-', 711 | + }, 712 | + 'celery_tracing': { 713 | + '()': 'asgi_correlation_id.CeleryTracingIdsFilter', 714 | + 'uuid_length': 32, 715 | + 'default_value': '-', 716 | + }, 717 | }, 718 | 'formatters': { 719 | 'web': { 720 | 'class': 'logging.Formatter', 721 | 'datefmt': '%H:%M:%S', 722 | 'format': '%(levelname)s ... [%(correlation_id)s] %(name)s %(message)s', 723 | }, 724 | + 'celery': { 725 | + 'class': 'logging.Formatter', 726 | + 'datefmt': '%H:%M:%S', 727 | + 'format': '%(levelname)s ... [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s %(message)s', 728 | + }, 729 | }, 730 | 'handlers': { 731 | 'web': { 732 | 'class': 'logging.StreamHandler', 733 | 'filters': ['correlation_id'], 734 | 'formatter': 'web', 735 | }, 736 | + 'celery': { 737 | + 'class': 'logging.StreamHandler', 738 | + 'filters': ['correlation_id', 'celery_tracing'], 739 | + 'formatter': 'celery', 740 | + }, 741 | }, 742 | 'loggers': { 743 | 'my_project': { 744 | + 'handlers': ['celery' if any('celery' in i for i in sys.argv) else 'web'], 745 | 'level': 'DEBUG', 746 | 'propagate': True, 747 | }, 748 | }, 749 | } 750 | ``` 751 | 752 | With these IDs configured you should be able to: 753 | 754 | 1. correlate all logs from a single origin, and 755 | 2. piece together the order each log was run, and which process spawned which 756 | 757 | #### Example 758 | 759 | With everything configured, assuming you have a set of tasks like this: 760 | 761 | ```python 762 | @celery.task() 763 | def debug_task() -> None: 764 | logger.info('Debug task 1') 765 | second_debug_task.delay() 766 | second_debug_task.delay() 767 | 768 | 769 | @celery.task() 770 | def second_debug_task() -> None: 771 | logger.info('Debug task 2') 772 | third_debug_task.delay() 773 | fourth_debug_task.delay() 774 | 775 | 776 | @celery.task() 777 | def third_debug_task() -> None: 778 | logger.info('Debug task 3') 779 | fourth_debug_task.delay() 780 | fourth_debug_task.delay() 781 | 782 | 783 | @celery.task() 784 | def fourth_debug_task() -> None: 785 | logger.info('Debug task 4') 786 | ``` 787 | 788 | your logs could look something like this: 789 | 790 | ``` 791 | correlation-id current-id 792 | | parent-id | 793 | | | | 794 | INFO [3b162382e1] [ - ] [93ddf3639c] project.tasks - Debug task 1 795 | INFO [3b162382e1] [93ddf3639c] [24046ab022] project.tasks - Debug task 2 796 | INFO [3b162382e1] [93ddf3639c] [cb5595a417] project.tasks - Debug task 2 797 | INFO [3b162382e1] [24046ab022] [08f5428a66] project.tasks - Debug task 3 798 | INFO [3b162382e1] [24046ab022] [32f40041c6] project.tasks - Debug task 4 799 | INFO [3b162382e1] [cb5595a417] [1c75a4ed2c] project.tasks - Debug task 3 800 | INFO [3b162382e1] [08f5428a66] [578ad2d141] project.tasks - Debug task 4 801 | INFO [3b162382e1] [cb5595a417] [21b2ef77ae] project.tasks - Debug task 4 802 | INFO [3b162382e1] [08f5428a66] [8cad7fc4d7] project.tasks - Debug task 4 803 | INFO [3b162382e1] [1c75a4ed2c] [72a43319f0] project.tasks - Debug task 4 804 | INFO [3b162382e1] [1c75a4ed2c] [ec3cf4113e] project.tasks - Debug task 4 805 | ``` 806 | -------------------------------------------------------------------------------- /asgi_correlation_id/__init__.py: -------------------------------------------------------------------------------- 1 | from asgi_correlation_id.context import celery_current_id, celery_parent_id, correlation_id 2 | from asgi_correlation_id.log_filters import CeleryTracingIdsFilter, CorrelationIdFilter 3 | from asgi_correlation_id.middleware import CorrelationIdMiddleware 4 | 5 | __all__ = ( 6 | 'CeleryTracingIdsFilter', 7 | 'CorrelationIdFilter', 8 | 'CorrelationIdMiddleware', 9 | 'correlation_id', 10 | 'celery_current_id', 11 | 'celery_parent_id', 12 | ) 13 | -------------------------------------------------------------------------------- /asgi_correlation_id/context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from typing import Optional 3 | 4 | # Middleware 5 | correlation_id: ContextVar[Optional[str]] = ContextVar('correlation_id', default=None) 6 | 7 | # Celery extension 8 | celery_parent_id: ContextVar[Optional[str]] = ContextVar('celery_parent', default=None) 9 | celery_current_id: ContextVar[Optional[str]] = ContextVar('celery_current', default=None) 10 | -------------------------------------------------------------------------------- /asgi_correlation_id/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snok/asgi-correlation-id/ed006dcc119447bf68a170bd1557f6015427213d/asgi_correlation_id/extensions/__init__.py -------------------------------------------------------------------------------- /asgi_correlation_id/extensions/celery.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Callable, Dict 2 | from uuid import uuid4 3 | 4 | from celery.signals import before_task_publish, task_postrun, task_prerun 5 | 6 | from asgi_correlation_id.extensions.sentry import get_sentry_extension 7 | 8 | if TYPE_CHECKING: 9 | from celery import Task 10 | 11 | uuid_hex_generator: Callable[[], str] = lambda: uuid4().hex 12 | 13 | 14 | def load_correlation_ids(header_key: str = 'CORRELATION_ID', generator: Callable[[], str] = uuid_hex_generator) -> None: 15 | """ 16 | Transfer correlation IDs from a HTTP request to a Celery worker, 17 | when spawned from a request. 18 | 19 | This is called as long as Celery is installed. 20 | """ 21 | from asgi_correlation_id.context import correlation_id 22 | 23 | sentry_extension = get_sentry_extension() 24 | 25 | @before_task_publish.connect(weak=False) 26 | def transfer_correlation_id(headers: Dict[str, str], **kwargs: Any) -> None: 27 | """ 28 | Transfer correlation ID from request thread to Celery worker, by adding 29 | it as a header. 30 | 31 | This way we're able to correlate work executed by Celery workers, back 32 | to the originating request, when there was one. 33 | """ 34 | cid = correlation_id.get() 35 | if cid: 36 | headers[header_key] = cid 37 | 38 | @task_prerun.connect(weak=False) 39 | def load_correlation_id(task: 'Task', **kwargs: Any) -> None: 40 | """ 41 | Set correlation ID from header if it exists. 42 | 43 | If it doesn't exist, generate a unique ID for the task anyway. 44 | """ 45 | id_value = task.request.get(header_key) 46 | if id_value: 47 | correlation_id.set(id_value) 48 | sentry_extension(id_value) 49 | else: 50 | generated_correlation_id = generator() 51 | correlation_id.set(generated_correlation_id) 52 | sentry_extension(generated_correlation_id) 53 | 54 | @task_postrun.connect(weak=False) 55 | def cleanup(**kwargs: Any) -> None: 56 | """ 57 | Clear context vars, to avoid re-using values in the next task. 58 | 59 | Context vars are cleared automatically in a HTTP request-setting, 60 | but must be manually reset for workers. 61 | """ 62 | correlation_id.set(None) 63 | 64 | 65 | def load_celery_current_and_parent_ids( 66 | header_key: str = 'CELERY_PARENT_ID', 67 | generator: Callable[[], str] = uuid_hex_generator, 68 | use_internal_celery_task_id: bool = False, 69 | ) -> None: 70 | """ 71 | Configure Celery event hooks for generating tracing IDs with depth. 72 | 73 | This is not called automatically by the middleware. 74 | To use this, users should manually run it during startup. 75 | """ 76 | from asgi_correlation_id.context import celery_current_id, celery_parent_id 77 | 78 | @before_task_publish.connect(weak=False) 79 | def publish_task_from_worker_or_request(headers: Dict[str, str], **kwargs: Any) -> None: 80 | """ 81 | Transfer the current ID to the next Celery worker, by adding 82 | it as a header. 83 | 84 | This way we're able to tell which process spawned the next task. 85 | """ 86 | current = celery_current_id.get() 87 | if current: 88 | headers[header_key] = current 89 | 90 | @task_prerun.connect(weak=False) 91 | def worker_prerun(task_id: str, task: 'Task', **kwargs: Any) -> None: 92 | """ 93 | Set current ID, and parent ID if it exists. 94 | """ 95 | parent_id = task.request.get(header_key) 96 | if parent_id: 97 | celery_parent_id.set(parent_id) 98 | 99 | celery_id = task_id if use_internal_celery_task_id else generator() 100 | celery_current_id.set(celery_id) 101 | 102 | @task_postrun.connect(weak=False) 103 | def clean_up(**kwargs: Any) -> None: 104 | """ 105 | Clear context vars, to avoid re-using values in the next task. 106 | """ 107 | celery_current_id.set(None) 108 | celery_parent_id.set(None) 109 | -------------------------------------------------------------------------------- /asgi_correlation_id/extensions/sentry.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | 4 | def get_sentry_extension() -> Callable[[str], None]: 5 | """ 6 | Return set_transaction_id, if the Sentry-sdk is installed. 7 | """ 8 | try: 9 | import sentry_sdk # noqa: F401, TC002 10 | 11 | from asgi_correlation_id.extensions.sentry import set_transaction_id 12 | 13 | return set_transaction_id 14 | except ImportError: # pragma: no cover 15 | return lambda correlation_id: None 16 | 17 | 18 | def set_transaction_id(correlation_id: str) -> None: 19 | """ 20 | Set Sentry's event transaction ID as the current correlation ID. 21 | 22 | The transaction ID is displayed in a Sentry event's detail view, 23 | which makes it easier to correlate logs to specific events. 24 | """ 25 | import sentry_sdk 26 | from packaging import version 27 | 28 | if version.parse(sentry_sdk.VERSION) >= version.parse('2.12.0'): 29 | scope = sentry_sdk.get_isolation_scope() 30 | scope.set_tag('transaction_id', correlation_id) 31 | else: 32 | with sentry_sdk.configure_scope() as scope: 33 | scope.set_tag('transaction_id', correlation_id) 34 | -------------------------------------------------------------------------------- /asgi_correlation_id/log_filters.py: -------------------------------------------------------------------------------- 1 | from logging import Filter 2 | from typing import TYPE_CHECKING, Optional 3 | 4 | from asgi_correlation_id.context import celery_current_id, celery_parent_id, correlation_id 5 | 6 | if TYPE_CHECKING: 7 | from logging import LogRecord 8 | 9 | 10 | def _trim_string(string: Optional[str], string_length: Optional[int]) -> Optional[str]: 11 | return string[:string_length] if string_length is not None and string else string 12 | 13 | 14 | # Middleware 15 | 16 | 17 | class CorrelationIdFilter(Filter): 18 | """Logging filter to attached correlation IDs to log records""" 19 | 20 | def __init__(self, name: str = '', uuid_length: Optional[int] = None, default_value: Optional[str] = None): 21 | super().__init__(name=name) 22 | self.uuid_length = uuid_length 23 | self.default_value = default_value 24 | 25 | def filter(self, record: 'LogRecord') -> bool: 26 | """ 27 | Attach a correlation ID to the log record. 28 | 29 | Since the correlation ID is defined in the middleware layer, any 30 | log generated from a request after this point can easily be searched 31 | for, if the correlation ID is added to the message, or included as 32 | metadata. 33 | """ 34 | cid = correlation_id.get(self.default_value) 35 | record.correlation_id = _trim_string(cid, self.uuid_length) 36 | return True 37 | 38 | 39 | # Celery extension 40 | 41 | 42 | class CeleryTracingIdsFilter(Filter): 43 | def __init__(self, name: str = '', uuid_length: Optional[int] = None, default_value: Optional[str] = None): 44 | super().__init__(name=name) 45 | self.uuid_length = uuid_length 46 | self.default_value = default_value 47 | 48 | def filter(self, record: 'LogRecord') -> bool: 49 | """ 50 | Append a parent- and current ID to the log record. 51 | 52 | The celery current ID is a unique ID generated for each new worker process. 53 | The celery parent ID is the current ID of the worker process that spawned 54 | the current process. If the worker process was spawned by a beat process 55 | or from an endpoint, the parent ID will be None. 56 | """ 57 | pid = celery_parent_id.get(self.default_value) 58 | record.celery_parent_id = _trim_string(pid, self.uuid_length) 59 | cid = celery_current_id.get(self.default_value) 60 | record.celery_current_id = _trim_string(cid, self.uuid_length) 61 | return True 62 | -------------------------------------------------------------------------------- /asgi_correlation_id/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dataclasses import dataclass, field 3 | from typing import TYPE_CHECKING, Callable, Optional 4 | from uuid import UUID, uuid4 5 | 6 | from starlette.datastructures import MutableHeaders 7 | 8 | from asgi_correlation_id.context import correlation_id 9 | from asgi_correlation_id.extensions.sentry import get_sentry_extension 10 | 11 | if TYPE_CHECKING: 12 | from starlette.types import ASGIApp, Message, Receive, Scope, Send 13 | 14 | logger = logging.getLogger('asgi_correlation_id') 15 | 16 | 17 | def is_valid_uuid4(uuid_: str) -> bool: 18 | """ 19 | Check whether a string is a valid v4 uuid. 20 | """ 21 | try: 22 | return UUID(uuid_).version == 4 23 | except ValueError: 24 | return False 25 | 26 | 27 | FAILED_VALIDATION_MESSAGE = 'Generated new request ID (%s), since request header value failed validation' 28 | 29 | 30 | @dataclass 31 | class CorrelationIdMiddleware: 32 | app: 'ASGIApp' 33 | header_name: str = 'X-Request-ID' 34 | update_request_header: bool = True 35 | 36 | # ID-generating callable 37 | generator: Callable[[], str] = field(default=lambda: uuid4().hex) 38 | 39 | # ID validator 40 | validator: Optional[Callable[[str], bool]] = field(default=is_valid_uuid4) 41 | 42 | # ID transformer - can be used to clean/mutate IDs 43 | transformer: Optional[Callable[[str], str]] = field(default=lambda a: a) 44 | 45 | async def __call__(self, scope: 'Scope', receive: 'Receive', send: 'Send') -> None: 46 | """ 47 | Load request ID from headers if present. Generate one otherwise. 48 | """ 49 | if scope['type'] not in ('http', 'websocket'): 50 | await self.app(scope, receive, send) 51 | return 52 | 53 | # Try to load request ID from the request headers 54 | headers = MutableHeaders(scope=scope) 55 | header_value = headers.get(self.header_name.lower()) 56 | 57 | validation_failed = False 58 | if not header_value: 59 | # Generate request ID if none was found 60 | id_value = self.generator() 61 | elif self.validator and not self.validator(header_value): 62 | # Also generate a request ID if one was found, but it was deemed invalid 63 | validation_failed = True 64 | id_value = self.generator() 65 | else: 66 | # Otherwise, use the found request ID 67 | id_value = header_value 68 | 69 | # Clean/change the ID if needed 70 | if self.transformer: 71 | id_value = self.transformer(id_value) 72 | 73 | if validation_failed is True: 74 | logger.warning(FAILED_VALIDATION_MESSAGE, id_value) 75 | 76 | # Update the request headers if needed 77 | if id_value != header_value and self.update_request_header is True: 78 | headers[self.header_name] = id_value 79 | 80 | correlation_id.set(id_value) 81 | self.sentry_extension(id_value) 82 | 83 | async def handle_outgoing_request(message: 'Message') -> None: 84 | if message['type'] == 'http.response.start' and correlation_id.get(): 85 | headers = MutableHeaders(scope=message) 86 | headers.append(self.header_name, correlation_id.get()) 87 | 88 | await send(message) 89 | 90 | await self.app(scope, receive, handle_outgoing_request) 91 | return 92 | 93 | def __post_init__(self) -> None: 94 | """ 95 | Load extensions on initialization. 96 | 97 | If Sentry is installed, propagate correlation IDs to Sentry events. 98 | If Celery is installed, propagate correlation IDs to spawned worker processes. 99 | """ 100 | self.sentry_extension = get_sentry_extension() 101 | try: 102 | import celery # noqa: F401, TC002 103 | 104 | from asgi_correlation_id.extensions.celery import load_correlation_ids 105 | 106 | load_correlation_ids() 107 | except ImportError: # pragma: no cover 108 | pass 109 | -------------------------------------------------------------------------------- /asgi_correlation_id/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snok/asgi-correlation-id/ed006dcc119447bf68a170bd1557f6015427213d/asgi_correlation_id/py.typed -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "amqp" 5 | version = "5.2.0" 6 | description = "Low-level AMQP client for Python (fork of amqplib)." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, 11 | {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, 12 | ] 13 | 14 | [package.dependencies] 15 | vine = ">=5.0.0,<6.0.0" 16 | 17 | [[package]] 18 | name = "annotated-types" 19 | version = "0.7.0" 20 | description = "Reusable constraint types to use with typing.Annotated" 21 | optional = false 22 | python-versions = ">=3.8" 23 | files = [ 24 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 25 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 26 | ] 27 | 28 | [package.dependencies] 29 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} 30 | 31 | [[package]] 32 | name = "anyio" 33 | version = "4.4.0" 34 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 35 | optional = false 36 | python-versions = ">=3.8" 37 | files = [ 38 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 39 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 40 | ] 41 | 42 | [package.dependencies] 43 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 44 | idna = ">=2.8" 45 | sniffio = ">=1.1" 46 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 47 | 48 | [package.extras] 49 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 50 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 51 | trio = ["trio (>=0.23)"] 52 | 53 | [[package]] 54 | name = "backports-zoneinfo" 55 | version = "0.2.1" 56 | description = "Backport of the standard library zoneinfo module" 57 | optional = false 58 | python-versions = ">=3.6" 59 | files = [ 60 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, 61 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, 62 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, 63 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, 64 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, 65 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, 66 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, 67 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, 68 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, 69 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, 70 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, 71 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, 72 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, 73 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, 74 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, 75 | {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, 76 | ] 77 | 78 | [package.dependencies] 79 | tzdata = {version = "*", optional = true, markers = "extra == \"tzdata\""} 80 | 81 | [package.extras] 82 | tzdata = ["tzdata"] 83 | 84 | [[package]] 85 | name = "billiard" 86 | version = "4.2.0" 87 | description = "Python multiprocessing fork with improvements and bugfixes" 88 | optional = false 89 | python-versions = ">=3.7" 90 | files = [ 91 | {file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"}, 92 | {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, 93 | ] 94 | 95 | [[package]] 96 | name = "celery" 97 | version = "5.4.0" 98 | description = "Distributed Task Queue." 99 | optional = false 100 | python-versions = ">=3.8" 101 | files = [ 102 | {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, 103 | {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, 104 | ] 105 | 106 | [package.dependencies] 107 | "backports.zoneinfo" = {version = ">=0.2.1", markers = "python_version < \"3.9\""} 108 | billiard = ">=4.2.0,<5.0" 109 | click = ">=8.1.2,<9.0" 110 | click-didyoumean = ">=0.3.0" 111 | click-plugins = ">=1.1.1" 112 | click-repl = ">=0.2.0" 113 | kombu = ">=5.3.4,<6.0" 114 | python-dateutil = ">=2.8.2" 115 | tzdata = ">=2022.7" 116 | vine = ">=5.1.0,<6.0" 117 | 118 | [package.extras] 119 | arangodb = ["pyArango (>=2.0.2)"] 120 | auth = ["cryptography (==42.0.5)"] 121 | azureblockblob = ["azure-storage-blob (>=12.15.0)"] 122 | brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] 123 | cassandra = ["cassandra-driver (>=3.25.0,<4)"] 124 | consul = ["python-consul2 (==0.1.5)"] 125 | cosmosdbsql = ["pydocumentdb (==2.3.5)"] 126 | couchbase = ["couchbase (>=3.0.0)"] 127 | couchdb = ["pycouchdb (==1.14.2)"] 128 | django = ["Django (>=2.2.28)"] 129 | dynamodb = ["boto3 (>=1.26.143)"] 130 | elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] 131 | eventlet = ["eventlet (>=0.32.0)"] 132 | gcs = ["google-cloud-storage (>=2.10.0)"] 133 | gevent = ["gevent (>=1.5.0)"] 134 | librabbitmq = ["librabbitmq (>=2.0.0)"] 135 | memcache = ["pylibmc (==1.6.3)"] 136 | mongodb = ["pymongo[srv] (>=4.0.2)"] 137 | msgpack = ["msgpack (==1.0.8)"] 138 | pymemcache = ["python-memcached (>=1.61)"] 139 | pyro = ["pyro4 (==4.82)"] 140 | pytest = ["pytest-celery[all] (>=1.0.0)"] 141 | redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] 142 | s3 = ["boto3 (>=1.26.143)"] 143 | slmq = ["softlayer-messaging (>=1.0.3)"] 144 | solar = ["ephem (==4.1.5)"] 145 | sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] 146 | sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] 147 | tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] 148 | yaml = ["PyYAML (>=3.10)"] 149 | zookeeper = ["kazoo (>=1.3.1)"] 150 | zstd = ["zstandard (==0.22.0)"] 151 | 152 | [[package]] 153 | name = "certifi" 154 | version = "2024.8.30" 155 | description = "Python package for providing Mozilla's CA Bundle." 156 | optional = false 157 | python-versions = ">=3.6" 158 | files = [ 159 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 160 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 161 | ] 162 | 163 | [[package]] 164 | name = "cfgv" 165 | version = "3.4.0" 166 | description = "Validate configuration and produce human readable error messages." 167 | optional = false 168 | python-versions = ">=3.8" 169 | files = [ 170 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 171 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 172 | ] 173 | 174 | [[package]] 175 | name = "charset-normalizer" 176 | version = "3.3.2" 177 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 178 | optional = false 179 | python-versions = ">=3.7.0" 180 | files = [ 181 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 182 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 183 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 184 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 185 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 186 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 187 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 188 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 189 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 190 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 191 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 192 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 193 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 194 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 195 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 196 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 197 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 198 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 199 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 200 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 201 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 202 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 203 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 204 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 205 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 206 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 207 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 208 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 209 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 210 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 211 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 212 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 213 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 214 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 215 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 216 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 217 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 218 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 219 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 220 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 221 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 222 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 223 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 224 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 225 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 226 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 227 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 228 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 229 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 230 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 231 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 232 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 233 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 234 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 235 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 236 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 237 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 238 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 239 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 240 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 241 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 242 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 243 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 244 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 245 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 246 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 247 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 248 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 249 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 250 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 251 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 252 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 253 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 254 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 255 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 256 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 257 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 258 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 259 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 260 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 261 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 262 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 263 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 264 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 265 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 266 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 267 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 268 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 269 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 270 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 271 | ] 272 | 273 | [[package]] 274 | name = "click" 275 | version = "8.1.7" 276 | description = "Composable command line interface toolkit" 277 | optional = false 278 | python-versions = ">=3.7" 279 | files = [ 280 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 281 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 282 | ] 283 | 284 | [package.dependencies] 285 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 286 | 287 | [[package]] 288 | name = "click-didyoumean" 289 | version = "0.3.1" 290 | description = "Enables git-like *did-you-mean* feature in click" 291 | optional = false 292 | python-versions = ">=3.6.2" 293 | files = [ 294 | {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, 295 | {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, 296 | ] 297 | 298 | [package.dependencies] 299 | click = ">=7" 300 | 301 | [[package]] 302 | name = "click-plugins" 303 | version = "1.1.1" 304 | description = "An extension module for click to enable registering CLI commands via setuptools entry-points." 305 | optional = false 306 | python-versions = "*" 307 | files = [ 308 | {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, 309 | {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, 310 | ] 311 | 312 | [package.dependencies] 313 | click = ">=4.0" 314 | 315 | [package.extras] 316 | dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] 317 | 318 | [[package]] 319 | name = "click-repl" 320 | version = "0.3.0" 321 | description = "REPL plugin for Click" 322 | optional = false 323 | python-versions = ">=3.6" 324 | files = [ 325 | {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, 326 | {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, 327 | ] 328 | 329 | [package.dependencies] 330 | click = ">=7.0" 331 | prompt-toolkit = ">=3.0.36" 332 | 333 | [package.extras] 334 | testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] 335 | 336 | [[package]] 337 | name = "colorama" 338 | version = "0.4.6" 339 | description = "Cross-platform colored terminal text." 340 | optional = false 341 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 342 | files = [ 343 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 344 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 345 | ] 346 | 347 | [[package]] 348 | name = "coverage" 349 | version = "6.5.0" 350 | description = "Code coverage measurement for Python" 351 | optional = false 352 | python-versions = ">=3.7" 353 | files = [ 354 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 355 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 356 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 357 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 358 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 359 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 360 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 361 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 362 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 363 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 364 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 365 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 366 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 367 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 368 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 369 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 370 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 371 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 372 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 373 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 374 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 375 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 376 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 377 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 378 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 379 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 380 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 381 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 382 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 383 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 384 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 385 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 386 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 387 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 388 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 389 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 390 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 391 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 392 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 393 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 394 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 395 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 396 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 397 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 398 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 399 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 400 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 401 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 402 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 403 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 404 | ] 405 | 406 | [package.dependencies] 407 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 408 | 409 | [package.extras] 410 | toml = ["tomli"] 411 | 412 | [[package]] 413 | name = "debugpy" 414 | version = "1.8.5" 415 | description = "An implementation of the Debug Adapter Protocol for Python" 416 | optional = false 417 | python-versions = ">=3.8" 418 | files = [ 419 | {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, 420 | {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, 421 | {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, 422 | {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, 423 | {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, 424 | {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, 425 | {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, 426 | {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, 427 | {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, 428 | {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, 429 | {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, 430 | {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, 431 | {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, 432 | {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, 433 | {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, 434 | {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, 435 | {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, 436 | {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, 437 | {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, 438 | {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, 439 | {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, 440 | {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, 441 | ] 442 | 443 | [[package]] 444 | name = "distlib" 445 | version = "0.3.8" 446 | description = "Distribution utilities" 447 | optional = false 448 | python-versions = "*" 449 | files = [ 450 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 451 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 452 | ] 453 | 454 | [[package]] 455 | name = "docker" 456 | version = "7.1.0" 457 | description = "A Python library for the Docker Engine API." 458 | optional = false 459 | python-versions = ">=3.8" 460 | files = [ 461 | {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, 462 | {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, 463 | ] 464 | 465 | [package.dependencies] 466 | pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} 467 | requests = ">=2.26.0" 468 | urllib3 = ">=1.26.0" 469 | 470 | [package.extras] 471 | dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] 472 | docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] 473 | ssh = ["paramiko (>=2.4.3)"] 474 | websockets = ["websocket-client (>=1.3.0)"] 475 | 476 | [[package]] 477 | name = "exceptiongroup" 478 | version = "1.2.2" 479 | description = "Backport of PEP 654 (exception groups)" 480 | optional = false 481 | python-versions = ">=3.7" 482 | files = [ 483 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 484 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 485 | ] 486 | 487 | [package.extras] 488 | test = ["pytest (>=6)"] 489 | 490 | [[package]] 491 | name = "fastapi" 492 | version = "0.112.2" 493 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 494 | optional = false 495 | python-versions = ">=3.8" 496 | files = [ 497 | {file = "fastapi-0.112.2-py3-none-any.whl", hash = "sha256:db84b470bd0e2b1075942231e90e3577e12a903c4dc8696f0d206a7904a7af1c"}, 498 | {file = "fastapi-0.112.2.tar.gz", hash = "sha256:3d4729c038414d5193840706907a41839d839523da6ed0c2811f1168cac1798c"}, 499 | ] 500 | 501 | [package.dependencies] 502 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 503 | starlette = ">=0.37.2,<0.39.0" 504 | typing-extensions = ">=4.8.0" 505 | 506 | [package.extras] 507 | all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 508 | standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] 509 | 510 | [[package]] 511 | name = "filelock" 512 | version = "3.15.4" 513 | description = "A platform independent file lock." 514 | optional = false 515 | python-versions = ">=3.8" 516 | files = [ 517 | {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, 518 | {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, 519 | ] 520 | 521 | [package.extras] 522 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 523 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] 524 | typing = ["typing-extensions (>=4.8)"] 525 | 526 | [[package]] 527 | name = "h11" 528 | version = "0.14.0" 529 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 530 | optional = false 531 | python-versions = ">=3.7" 532 | files = [ 533 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 534 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 535 | ] 536 | 537 | [[package]] 538 | name = "httpcore" 539 | version = "1.0.5" 540 | description = "A minimal low-level HTTP client." 541 | optional = false 542 | python-versions = ">=3.8" 543 | files = [ 544 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 545 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 546 | ] 547 | 548 | [package.dependencies] 549 | certifi = "*" 550 | h11 = ">=0.13,<0.15" 551 | 552 | [package.extras] 553 | asyncio = ["anyio (>=4.0,<5.0)"] 554 | http2 = ["h2 (>=3,<5)"] 555 | socks = ["socksio (==1.*)"] 556 | trio = ["trio (>=0.22.0,<0.26.0)"] 557 | 558 | [[package]] 559 | name = "httpx" 560 | version = "0.27.2" 561 | description = "The next generation HTTP client." 562 | optional = false 563 | python-versions = ">=3.8" 564 | files = [ 565 | {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, 566 | {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, 567 | ] 568 | 569 | [package.dependencies] 570 | anyio = "*" 571 | certifi = "*" 572 | httpcore = "==1.*" 573 | idna = "*" 574 | sniffio = "*" 575 | 576 | [package.extras] 577 | brotli = ["brotli", "brotlicffi"] 578 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 579 | http2 = ["h2 (>=3,<5)"] 580 | socks = ["socksio (==1.*)"] 581 | zstd = ["zstandard (>=0.18.0)"] 582 | 583 | [[package]] 584 | name = "identify" 585 | version = "2.6.0" 586 | description = "File identification library for Python" 587 | optional = false 588 | python-versions = ">=3.8" 589 | files = [ 590 | {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, 591 | {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, 592 | ] 593 | 594 | [package.extras] 595 | license = ["ukkonen"] 596 | 597 | [[package]] 598 | name = "idna" 599 | version = "3.8" 600 | description = "Internationalized Domain Names in Applications (IDNA)" 601 | optional = false 602 | python-versions = ">=3.6" 603 | files = [ 604 | {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, 605 | {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, 606 | ] 607 | 608 | [[package]] 609 | name = "iniconfig" 610 | version = "2.0.0" 611 | description = "brain-dead simple config-ini parsing" 612 | optional = false 613 | python-versions = ">=3.7" 614 | files = [ 615 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 616 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 617 | ] 618 | 619 | [[package]] 620 | name = "kombu" 621 | version = "5.4.0" 622 | description = "Messaging library for Python." 623 | optional = false 624 | python-versions = ">=3.8" 625 | files = [ 626 | {file = "kombu-5.4.0-py3-none-any.whl", hash = "sha256:c8dd99820467610b4febbc7a9e8a0d3d7da2d35116b67184418b51cc520ea6b6"}, 627 | {file = "kombu-5.4.0.tar.gz", hash = "sha256:ad200a8dbdaaa2bbc5f26d2ee7d707d9a1fded353a0f4bd751ce8c7d9f449c60"}, 628 | ] 629 | 630 | [package.dependencies] 631 | amqp = ">=5.1.1,<6.0.0" 632 | "backports.zoneinfo" = {version = ">=0.2.1", extras = ["tzdata"], markers = "python_version < \"3.9\""} 633 | typing-extensions = {version = "4.12.2", markers = "python_version < \"3.10\""} 634 | vine = "5.1.0" 635 | 636 | [package.extras] 637 | azureservicebus = ["azure-servicebus (>=7.10.0)"] 638 | azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] 639 | confluentkafka = ["confluent-kafka (>=2.2.0)"] 640 | consul = ["python-consul2 (==0.1.5)"] 641 | librabbitmq = ["librabbitmq (>=2.0.0)"] 642 | mongodb = ["pymongo (>=4.1.1)"] 643 | msgpack = ["msgpack (==1.0.8)"] 644 | pyro = ["pyro4 (==4.82)"] 645 | qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] 646 | redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] 647 | slmq = ["softlayer-messaging (>=1.0.3)"] 648 | sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] 649 | sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] 650 | yaml = ["PyYAML (>=3.10)"] 651 | zookeeper = ["kazoo (>=2.8.0)"] 652 | 653 | [[package]] 654 | name = "nodeenv" 655 | version = "1.9.1" 656 | description = "Node.js virtual environment builder" 657 | optional = false 658 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 659 | files = [ 660 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 661 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 662 | ] 663 | 664 | [[package]] 665 | name = "packaging" 666 | version = "24.1" 667 | description = "Core utilities for Python packages" 668 | optional = false 669 | python-versions = ">=3.8" 670 | files = [ 671 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 672 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 673 | ] 674 | 675 | [[package]] 676 | name = "platformdirs" 677 | version = "4.2.2" 678 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 679 | optional = false 680 | python-versions = ">=3.8" 681 | files = [ 682 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 683 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 684 | ] 685 | 686 | [package.extras] 687 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 688 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 689 | type = ["mypy (>=1.8)"] 690 | 691 | [[package]] 692 | name = "pluggy" 693 | version = "1.5.0" 694 | description = "plugin and hook calling mechanisms for python" 695 | optional = false 696 | python-versions = ">=3.8" 697 | files = [ 698 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 699 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 700 | ] 701 | 702 | [package.extras] 703 | dev = ["pre-commit", "tox"] 704 | testing = ["pytest", "pytest-benchmark"] 705 | 706 | [[package]] 707 | name = "pre-commit" 708 | version = "3.5.0" 709 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 710 | optional = false 711 | python-versions = ">=3.8" 712 | files = [ 713 | {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, 714 | {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, 715 | ] 716 | 717 | [package.dependencies] 718 | cfgv = ">=2.0.0" 719 | identify = ">=1.0.0" 720 | nodeenv = ">=0.11.1" 721 | pyyaml = ">=5.1" 722 | virtualenv = ">=20.10.0" 723 | 724 | [[package]] 725 | name = "prompt-toolkit" 726 | version = "3.0.47" 727 | description = "Library for building powerful interactive command lines in Python" 728 | optional = false 729 | python-versions = ">=3.7.0" 730 | files = [ 731 | {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, 732 | {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, 733 | ] 734 | 735 | [package.dependencies] 736 | wcwidth = "*" 737 | 738 | [[package]] 739 | name = "psutil" 740 | version = "6.0.0" 741 | description = "Cross-platform lib for process and system monitoring in Python." 742 | optional = false 743 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 744 | files = [ 745 | {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, 746 | {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, 747 | {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, 748 | {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, 749 | {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, 750 | {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, 751 | {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, 752 | {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, 753 | {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, 754 | {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, 755 | {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, 756 | {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, 757 | {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, 758 | {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, 759 | {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, 760 | {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, 761 | {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, 762 | ] 763 | 764 | [package.extras] 765 | test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] 766 | 767 | [[package]] 768 | name = "pydantic" 769 | version = "2.8.2" 770 | description = "Data validation using Python type hints" 771 | optional = false 772 | python-versions = ">=3.8" 773 | files = [ 774 | {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, 775 | {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, 776 | ] 777 | 778 | [package.dependencies] 779 | annotated-types = ">=0.4.0" 780 | pydantic-core = "2.20.1" 781 | typing-extensions = [ 782 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 783 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 784 | ] 785 | 786 | [package.extras] 787 | email = ["email-validator (>=2.0.0)"] 788 | 789 | [[package]] 790 | name = "pydantic-core" 791 | version = "2.20.1" 792 | description = "Core functionality for Pydantic validation and serialization" 793 | optional = false 794 | python-versions = ">=3.8" 795 | files = [ 796 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, 797 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, 798 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, 799 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, 800 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, 801 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, 802 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, 803 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, 804 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, 805 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, 806 | {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, 807 | {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, 808 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, 809 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, 810 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, 811 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, 812 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, 813 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, 814 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, 815 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, 816 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, 817 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, 818 | {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, 819 | {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, 820 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, 821 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, 822 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, 823 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, 824 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, 825 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, 826 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, 827 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, 828 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, 829 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, 830 | {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, 831 | {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, 832 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, 833 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, 834 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, 835 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, 836 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, 837 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, 838 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, 839 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, 840 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, 841 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, 842 | {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, 843 | {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, 844 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, 845 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, 846 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, 847 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, 848 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, 849 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, 850 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, 851 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, 852 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, 853 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, 854 | {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, 855 | {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, 856 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, 857 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, 858 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, 859 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, 860 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, 861 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, 862 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, 863 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, 864 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, 865 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, 866 | {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, 867 | {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, 868 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, 869 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, 870 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, 871 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, 872 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, 873 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, 874 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, 875 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, 876 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, 877 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, 878 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, 879 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, 880 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, 881 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, 882 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, 883 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, 884 | {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, 885 | ] 886 | 887 | [package.dependencies] 888 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 889 | 890 | [[package]] 891 | name = "pytest" 892 | version = "8.3.2" 893 | description = "pytest: simple powerful testing with Python" 894 | optional = false 895 | python-versions = ">=3.8" 896 | files = [ 897 | {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, 898 | {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, 899 | ] 900 | 901 | [package.dependencies] 902 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 903 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 904 | iniconfig = "*" 905 | packaging = "*" 906 | pluggy = ">=1.5,<2" 907 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 908 | 909 | [package.extras] 910 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 911 | 912 | [[package]] 913 | name = "pytest-asyncio" 914 | version = "0.24.0" 915 | description = "Pytest support for asyncio" 916 | optional = false 917 | python-versions = ">=3.8" 918 | files = [ 919 | {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, 920 | {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, 921 | ] 922 | 923 | [package.dependencies] 924 | pytest = ">=8.2,<9" 925 | 926 | [package.extras] 927 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 928 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 929 | 930 | [[package]] 931 | name = "pytest-celery" 932 | version = "1.1.1" 933 | description = "Pytest plugin for Celery" 934 | optional = false 935 | python-versions = "<4.0,>=3.8" 936 | files = [ 937 | {file = "pytest_celery-1.1.1-py3-none-any.whl", hash = "sha256:0b441f2a1caea29a34d87880b967a853fd36e65e44ce81ffe7f5af1bfe80768d"}, 938 | {file = "pytest_celery-1.1.1.tar.gz", hash = "sha256:21e89e191a6a2713c76b4d3307a2fea9751a7b71c22ca09c531873034d2b92e8"}, 939 | ] 940 | 941 | [package.dependencies] 942 | celery = "*" 943 | debugpy = ">=1.8.5,<2.0.0" 944 | docker = ">=7.1.0,<8.0.0" 945 | psutil = ">=6.0.0" 946 | pytest-docker-tools = ">=3.1.3" 947 | setuptools = ">=72.1.0" 948 | tenacity = ">=9.0.0" 949 | 950 | [package.extras] 951 | all = ["boto3", "botocore", "pycurl", "python-memcached", "redis", "urllib3"] 952 | memcached = ["python-memcached"] 953 | redis = ["redis"] 954 | sqs = ["boto3", "botocore", "pycurl", "urllib3"] 955 | 956 | [[package]] 957 | name = "pytest-docker-tools" 958 | version = "3.1.3" 959 | description = "Docker integration tests for pytest" 960 | optional = false 961 | python-versions = ">=3.7.0,<4.0.0" 962 | files = [ 963 | {file = "pytest_docker_tools-3.1.3-py3-none-any.whl", hash = "sha256:63e659043160f41d89f94ea42616102594bcc85682aac394fcbc14f14cd1b189"}, 964 | {file = "pytest_docker_tools-3.1.3.tar.gz", hash = "sha256:c7e28841839d67b3ac80ad7b345b953701d5ae61ffda97586114244292aeacc0"}, 965 | ] 966 | 967 | [package.dependencies] 968 | docker = ">=4.3.1" 969 | pytest = ">=6.0.1" 970 | 971 | [[package]] 972 | name = "pytest-mock" 973 | version = "3.14.0" 974 | description = "Thin-wrapper around the mock package for easier use with pytest" 975 | optional = false 976 | python-versions = ">=3.8" 977 | files = [ 978 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 979 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 980 | ] 981 | 982 | [package.dependencies] 983 | pytest = ">=6.2.5" 984 | 985 | [package.extras] 986 | dev = ["pre-commit", "pytest-asyncio", "tox"] 987 | 988 | [[package]] 989 | name = "python-dateutil" 990 | version = "2.9.0.post0" 991 | description = "Extensions to the standard Python datetime module" 992 | optional = false 993 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 994 | files = [ 995 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 996 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 997 | ] 998 | 999 | [package.dependencies] 1000 | six = ">=1.5" 1001 | 1002 | [[package]] 1003 | name = "pywin32" 1004 | version = "306" 1005 | description = "Python for Window Extensions" 1006 | optional = false 1007 | python-versions = "*" 1008 | files = [ 1009 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, 1010 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, 1011 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, 1012 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, 1013 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, 1014 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, 1015 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, 1016 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, 1017 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, 1018 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, 1019 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, 1020 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, 1021 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, 1022 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "pyyaml" 1027 | version = "6.0.2" 1028 | description = "YAML parser and emitter for Python" 1029 | optional = false 1030 | python-versions = ">=3.8" 1031 | files = [ 1032 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 1033 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 1034 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 1035 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 1036 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 1037 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 1038 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 1039 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 1040 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 1041 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 1042 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 1043 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 1044 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 1045 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 1046 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 1047 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 1048 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 1049 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 1050 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 1051 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 1052 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 1053 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 1054 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 1055 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 1056 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 1057 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 1058 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 1059 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 1060 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 1061 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 1062 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 1063 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 1064 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 1065 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 1066 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 1067 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 1068 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 1069 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 1070 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 1071 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 1072 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 1073 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 1074 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 1075 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 1076 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 1077 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 1078 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 1079 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 1080 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 1081 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 1082 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 1083 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 1084 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "requests" 1089 | version = "2.32.3" 1090 | description = "Python HTTP for Humans." 1091 | optional = false 1092 | python-versions = ">=3.8" 1093 | files = [ 1094 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 1095 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 1096 | ] 1097 | 1098 | [package.dependencies] 1099 | certifi = ">=2017.4.17" 1100 | charset-normalizer = ">=2,<4" 1101 | idna = ">=2.5,<4" 1102 | urllib3 = ">=1.21.1,<3" 1103 | 1104 | [package.extras] 1105 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1106 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1107 | 1108 | [[package]] 1109 | name = "sentry-sdk" 1110 | version = "2.13.0" 1111 | description = "Python client for Sentry (https://sentry.io)" 1112 | optional = false 1113 | python-versions = ">=3.6" 1114 | files = [ 1115 | {file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"}, 1116 | {file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"}, 1117 | ] 1118 | 1119 | [package.dependencies] 1120 | certifi = "*" 1121 | urllib3 = ">=1.26.11" 1122 | 1123 | [package.extras] 1124 | aiohttp = ["aiohttp (>=3.5)"] 1125 | anthropic = ["anthropic (>=0.16)"] 1126 | arq = ["arq (>=0.23)"] 1127 | asyncpg = ["asyncpg (>=0.23)"] 1128 | beam = ["apache-beam (>=2.12)"] 1129 | bottle = ["bottle (>=0.12.13)"] 1130 | celery = ["celery (>=3)"] 1131 | celery-redbeat = ["celery-redbeat (>=2)"] 1132 | chalice = ["chalice (>=1.16.0)"] 1133 | clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] 1134 | django = ["django (>=1.8)"] 1135 | falcon = ["falcon (>=1.4)"] 1136 | fastapi = ["fastapi (>=0.79.0)"] 1137 | flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] 1138 | grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] 1139 | httpx = ["httpx (>=0.16.0)"] 1140 | huey = ["huey (>=2)"] 1141 | huggingface-hub = ["huggingface-hub (>=0.22)"] 1142 | langchain = ["langchain (>=0.0.210)"] 1143 | litestar = ["litestar (>=2.0.0)"] 1144 | loguru = ["loguru (>=0.5)"] 1145 | openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] 1146 | opentelemetry = ["opentelemetry-distro (>=0.35b0)"] 1147 | opentelemetry-experimental = ["opentelemetry-distro"] 1148 | pure-eval = ["asttokens", "executing", "pure-eval"] 1149 | pymongo = ["pymongo (>=3.1)"] 1150 | pyspark = ["pyspark (>=2.4.4)"] 1151 | quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] 1152 | rq = ["rq (>=0.6)"] 1153 | sanic = ["sanic (>=0.8)"] 1154 | sqlalchemy = ["sqlalchemy (>=1.2)"] 1155 | starlette = ["starlette (>=0.19.1)"] 1156 | starlite = ["starlite (>=1.48)"] 1157 | tornado = ["tornado (>=6)"] 1158 | 1159 | [[package]] 1160 | name = "setuptools" 1161 | version = "74.0.0" 1162 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 1163 | optional = false 1164 | python-versions = ">=3.8" 1165 | files = [ 1166 | {file = "setuptools-74.0.0-py3-none-any.whl", hash = "sha256:0274581a0037b638b9fc1c6883cc71c0210865aaa76073f7882376b641b84e8f"}, 1167 | {file = "setuptools-74.0.0.tar.gz", hash = "sha256:a85e96b8be2b906f3e3e789adec6a9323abf79758ecfa3065bd740d81158b11e"}, 1168 | ] 1169 | 1170 | [package.extras] 1171 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] 1172 | core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] 1173 | cover = ["pytest-cov"] 1174 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 1175 | enabler = ["pytest-enabler (>=2.2)"] 1176 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] 1177 | type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] 1178 | 1179 | [[package]] 1180 | name = "six" 1181 | version = "1.16.0" 1182 | description = "Python 2 and 3 compatibility utilities" 1183 | optional = false 1184 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1185 | files = [ 1186 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1187 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "sniffio" 1192 | version = "1.3.1" 1193 | description = "Sniff out which async library your code is running under" 1194 | optional = false 1195 | python-versions = ">=3.7" 1196 | files = [ 1197 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 1198 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "starlette" 1203 | version = "0.38.4" 1204 | description = "The little ASGI library that shines." 1205 | optional = false 1206 | python-versions = ">=3.8" 1207 | files = [ 1208 | {file = "starlette-0.38.4-py3-none-any.whl", hash = "sha256:526f53a77f0e43b85f583438aee1a940fd84f8fd610353e8b0c1a77ad8a87e76"}, 1209 | {file = "starlette-0.38.4.tar.gz", hash = "sha256:53a7439060304a208fea17ed407e998f46da5e5d9b1addfea3040094512a6379"}, 1210 | ] 1211 | 1212 | [package.dependencies] 1213 | anyio = ">=3.4.0,<5" 1214 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 1215 | 1216 | [package.extras] 1217 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 1218 | 1219 | [[package]] 1220 | name = "tenacity" 1221 | version = "9.0.0" 1222 | description = "Retry code until it succeeds" 1223 | optional = false 1224 | python-versions = ">=3.8" 1225 | files = [ 1226 | {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, 1227 | {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, 1228 | ] 1229 | 1230 | [package.extras] 1231 | doc = ["reno", "sphinx"] 1232 | test = ["pytest", "tornado (>=4.5)", "typeguard"] 1233 | 1234 | [[package]] 1235 | name = "tomli" 1236 | version = "2.0.1" 1237 | description = "A lil' TOML parser" 1238 | optional = false 1239 | python-versions = ">=3.7" 1240 | files = [ 1241 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1242 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1243 | ] 1244 | 1245 | [[package]] 1246 | name = "typing-extensions" 1247 | version = "4.12.2" 1248 | description = "Backported and Experimental Type Hints for Python 3.8+" 1249 | optional = false 1250 | python-versions = ">=3.8" 1251 | files = [ 1252 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 1253 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "tzdata" 1258 | version = "2024.1" 1259 | description = "Provider of IANA time zone data" 1260 | optional = false 1261 | python-versions = ">=2" 1262 | files = [ 1263 | {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, 1264 | {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "urllib3" 1269 | version = "2.2.2" 1270 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1271 | optional = false 1272 | python-versions = ">=3.8" 1273 | files = [ 1274 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 1275 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 1276 | ] 1277 | 1278 | [package.extras] 1279 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1280 | h2 = ["h2 (>=4,<5)"] 1281 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1282 | zstd = ["zstandard (>=0.18.0)"] 1283 | 1284 | [[package]] 1285 | name = "vine" 1286 | version = "5.1.0" 1287 | description = "Python promises." 1288 | optional = false 1289 | python-versions = ">=3.6" 1290 | files = [ 1291 | {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, 1292 | {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "virtualenv" 1297 | version = "20.26.3" 1298 | description = "Virtual Python Environment builder" 1299 | optional = false 1300 | python-versions = ">=3.7" 1301 | files = [ 1302 | {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, 1303 | {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, 1304 | ] 1305 | 1306 | [package.dependencies] 1307 | distlib = ">=0.3.7,<1" 1308 | filelock = ">=3.12.2,<4" 1309 | platformdirs = ">=3.9.1,<5" 1310 | 1311 | [package.extras] 1312 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1313 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1314 | 1315 | [[package]] 1316 | name = "wcwidth" 1317 | version = "0.2.13" 1318 | description = "Measures the displayed width of unicode strings in a terminal" 1319 | optional = false 1320 | python-versions = "*" 1321 | files = [ 1322 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 1323 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "websockets" 1328 | version = "13.0.1" 1329 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 1330 | optional = false 1331 | python-versions = ">=3.8" 1332 | files = [ 1333 | {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f"}, 1334 | {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c"}, 1335 | {file = "websockets-13.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f"}, 1336 | {file = "websockets-13.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543"}, 1337 | {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d"}, 1338 | {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f"}, 1339 | {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8"}, 1340 | {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b"}, 1341 | {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448"}, 1342 | {file = "websockets-13.0.1-cp310-cp310-win32.whl", hash = "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3"}, 1343 | {file = "websockets-13.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0"}, 1344 | {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7"}, 1345 | {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4"}, 1346 | {file = "websockets-13.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2"}, 1347 | {file = "websockets-13.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0"}, 1348 | {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e"}, 1349 | {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462"}, 1350 | {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501"}, 1351 | {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418"}, 1352 | {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df"}, 1353 | {file = "websockets-13.0.1-cp311-cp311-win32.whl", hash = "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f"}, 1354 | {file = "websockets-13.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075"}, 1355 | {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a"}, 1356 | {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956"}, 1357 | {file = "websockets-13.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af"}, 1358 | {file = "websockets-13.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf"}, 1359 | {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c"}, 1360 | {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4"}, 1361 | {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab"}, 1362 | {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d"}, 1363 | {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237"}, 1364 | {file = "websockets-13.0.1-cp312-cp312-win32.whl", hash = "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185"}, 1365 | {file = "websockets-13.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99"}, 1366 | {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa"}, 1367 | {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231"}, 1368 | {file = "websockets-13.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9"}, 1369 | {file = "websockets-13.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75"}, 1370 | {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553"}, 1371 | {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920"}, 1372 | {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329"}, 1373 | {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7"}, 1374 | {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2"}, 1375 | {file = "websockets-13.0.1-cp313-cp313-win32.whl", hash = "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb"}, 1376 | {file = "websockets-13.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b"}, 1377 | {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e"}, 1378 | {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2"}, 1379 | {file = "websockets-13.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b"}, 1380 | {file = "websockets-13.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae"}, 1381 | {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f"}, 1382 | {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603"}, 1383 | {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36"}, 1384 | {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a"}, 1385 | {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb"}, 1386 | {file = "websockets-13.0.1-cp38-cp38-win32.whl", hash = "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4"}, 1387 | {file = "websockets-13.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9"}, 1388 | {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc"}, 1389 | {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63"}, 1390 | {file = "websockets-13.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37"}, 1391 | {file = "websockets-13.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9"}, 1392 | {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97"}, 1393 | {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f"}, 1394 | {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4"}, 1395 | {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0"}, 1396 | {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d"}, 1397 | {file = "websockets-13.0.1-cp39-cp39-win32.whl", hash = "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58"}, 1398 | {file = "websockets-13.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097"}, 1399 | {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89"}, 1400 | {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad"}, 1401 | {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e"}, 1402 | {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc"}, 1403 | {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333"}, 1404 | {file = "websockets-13.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32"}, 1405 | {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491"}, 1406 | {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f"}, 1407 | {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060"}, 1408 | {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491"}, 1409 | {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026"}, 1410 | {file = "websockets-13.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096"}, 1411 | {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376"}, 1412 | {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83"}, 1413 | {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c"}, 1414 | {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870"}, 1415 | {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5"}, 1416 | {file = "websockets-13.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980"}, 1417 | {file = "websockets-13.0.1-py3-none-any.whl", hash = "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817"}, 1418 | {file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"}, 1419 | ] 1420 | 1421 | [extras] 1422 | celery = ["celery"] 1423 | 1424 | [metadata] 1425 | lock-version = "2.0" 1426 | python-versions = "^3.8" 1427 | content-hash = "2c855b530235787ae7f1215cccb0d5c3a48c5ba04dfbb347c74e8c652a25d8c8" 1428 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "asgi-correlation-id" 3 | version = "4.3.4" 4 | description = "Middleware correlating project logs to individual requests" 5 | authors = ["Sondre Lillebø Gundersen "] 6 | maintainers = ["Jonas Krüger Svensson "] 7 | license = "MIT" 8 | readme = "README.md" 9 | homepage = "https://github.com/snok/asgi-correlation-id" 10 | repository = "https://github.com/snok/asgi-correlation-id" 11 | keywords = [ 12 | 'asgi', 13 | 'fastapi', 14 | 'starlette', 15 | 'async', 16 | 'correlation', 17 | 'correlation-id', 18 | 'request-id', 19 | 'x-request-id', 20 | 'tracing', 21 | 'logging', 22 | 'middleware', 23 | 'sentry', 24 | 'celery' 25 | ] 26 | classifiers = [ 27 | 'Development Status :: 5 - Production/Stable', 28 | 'Environment :: Web Environment', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3.8', 34 | 'Programming Language :: Python :: 3.9', 35 | 'Programming Language :: Python :: 3.10', 36 | 'Programming Language :: Python :: 3.11', 37 | 'Programming Language :: Python :: 3.12', 38 | 'Topic :: Internet :: WWW/HTTP', 39 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 40 | 'Topic :: Software Development', 41 | 'Topic :: Software Development :: Libraries', 42 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 43 | 'Topic :: Software Development :: Libraries :: Python Modules', 44 | ] 45 | packages = [ 46 | { include = "asgi_correlation_id" }, 47 | ] 48 | 49 | [tool.poetry.dependencies] 50 | python = "^3.8" 51 | starlette = ">=0.18" 52 | packaging = "*" 53 | celery = { version = '*', optional = true } 54 | 55 | [tool.poetry.dev-dependencies] 56 | pre-commit = "*" 57 | pytest = "*" 58 | pytest-mock = "*" 59 | sentry-sdk = "*" 60 | pytest-asyncio = "*" 61 | fastapi = "*" 62 | httpx = "*" 63 | coverage = [ 64 | { extras = ["toml"], version = "^6", python = ">=3.10" }, 65 | { version = "^5 || ^6" }, 66 | ] 67 | requests = "*" 68 | websockets = "*" 69 | pytest-celery = "*" 70 | 71 | [tool.poetry.extras] 72 | celery = ['celery'] 73 | 74 | [build-system] 75 | requires = ["poetry>=0.12"] 76 | build-backend = "poetry.masonry.api" 77 | 78 | [tool.black] 79 | quiet = true 80 | line-length = 120 81 | skip-string-normalization = true 82 | preview = true 83 | 84 | [tool.isort] 85 | profile = "black" 86 | line_length = 120 87 | 88 | [tool.coverage.run] 89 | omit = [] 90 | 91 | [tool.coverage.report] 92 | show_missing = true 93 | skip_covered = true 94 | exclude_lines = [ 95 | "if TYPE_CHECKING:", 96 | "pragma: no cover", 97 | ] 98 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | asyncio_mode = auto 4 | 5 | [flake8] 6 | max-line-length = 120 7 | max-complexity = 15 8 | enable-extensions = TC, TC2 9 | type-checking-exempt-modules = typing 10 | 11 | [mypy] 12 | python_version = 3.7 13 | strict = True 14 | show_error_codes = True 15 | warn_unused_ignores = True 16 | warn_redundant_casts = True 17 | warn_unused_configs = True 18 | check_untyped_defs = True 19 | disallow_untyped_decorators = False 20 | 21 | [mypy-tests.*] 22 | ignore_errors = True 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snok/asgi-correlation-id/ed006dcc119447bf68a170bd1557f6015427213d/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging.config import dictConfig 3 | 4 | import pytest 5 | import pytest_asyncio 6 | from fastapi import FastAPI 7 | from httpx import AsyncClient 8 | from starlette.middleware import Middleware 9 | 10 | from asgi_correlation_id.middleware import CorrelationIdMiddleware 11 | 12 | 13 | @pytest.fixture(autouse=True, scope='session') 14 | def _configure_logging(): 15 | LOGGING = { 16 | 'version': 1, 17 | 'disable_existing_loggers': False, 18 | 'filters': { 19 | 'correlation_id': {'()': 'asgi_correlation_id.CorrelationIdFilter'}, 20 | 'celery_tracing': {'()': 'asgi_correlation_id.CeleryTracingIdsFilter'}, 21 | }, 22 | 'formatters': { 23 | 'full': { 24 | 'class': 'logging.Formatter', 25 | 'datefmt': '%H:%M:%S', 26 | 'format': '[%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(message)s', 27 | }, 28 | }, 29 | 'handlers': { 30 | 'console': { 31 | 'class': 'logging.StreamHandler', 32 | 'filters': ['correlation_id', 'celery_tracing'], 33 | 'formatter': 'full', 34 | }, 35 | }, 36 | 'loggers': { 37 | # project logger 38 | 'asgi_correlation_id': { 39 | 'handlers': ['console'], 40 | 'level': 'DEBUG', 41 | 'propagate': True, 42 | }, 43 | }, 44 | } 45 | dictConfig(LOGGING) 46 | 47 | 48 | TRANSFORMER_VALUE = 'some-id' 49 | 50 | default_app = FastAPI(middleware=[Middleware(CorrelationIdMiddleware)]) 51 | update_request_header_app = FastAPI(middleware=[Middleware(CorrelationIdMiddleware, update_request_header=True)]) 52 | no_validator_or_transformer_app = FastAPI( 53 | middleware=[Middleware(CorrelationIdMiddleware, validator=None, transformer=None)] 54 | ) 55 | transformer_app = FastAPI(middleware=[Middleware(CorrelationIdMiddleware, transformer=lambda a: a * 2)]) 56 | generator_app = FastAPI(middleware=[Middleware(CorrelationIdMiddleware, generator=lambda: TRANSFORMER_VALUE)]) 57 | 58 | 59 | @pytest.fixture(scope='session', autouse=True) 60 | def event_loop(): 61 | loop = asyncio.get_event_loop_policy().new_event_loop() 62 | yield loop 63 | loop.close() 64 | 65 | 66 | @pytest_asyncio.fixture(scope='module') 67 | async def client() -> AsyncClient: 68 | async with AsyncClient(app=default_app, base_url='http://test') as client: 69 | yield client 70 | -------------------------------------------------------------------------------- /tests/test_extension_celery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | from uuid import UUID, uuid4 4 | 5 | import pytest 6 | from celery import shared_task 7 | 8 | from asgi_correlation_id.extensions.celery import load_celery_current_and_parent_ids, load_correlation_ids 9 | from tests.conftest import default_app 10 | 11 | logger = logging.getLogger('asgi_correlation_id') 12 | 13 | pytestmark = pytest.mark.asyncio 14 | 15 | # Configure Celery signals 16 | load_correlation_ids() 17 | load_celery_current_and_parent_ids() 18 | 19 | 20 | @shared_task 21 | def task1(): 22 | logger.info('test1') 23 | task2.delay() 24 | 25 | 26 | @shared_task() 27 | def task2(): 28 | logger.info('test2') 29 | task3.delay() 30 | 31 | 32 | @shared_task() 33 | def task3(): 34 | logger.info('test3') 35 | 36 | 37 | async def test_endpoint_to_worker_to_worker(client, caplog, celery_session_app, celery_session_worker): 38 | """ 39 | We expect: 40 | - The correlation ID to persist from the endpoint to the final worker 41 | - The current ID of the first worker to be added as the parent ID of the second worker 42 | """ 43 | 44 | @default_app.get('/celery-test', status_code=200) 45 | async def test_view(): 46 | logger.debug('Test view') 47 | task1.delay().get(timeout=10) 48 | 49 | caplog.set_level('DEBUG') 50 | 51 | cid = uuid4().hex 52 | 53 | with warnings.catch_warnings(): 54 | warnings.simplefilter('ignore') 55 | await client.get('celery-test', headers={'X-Request-ID': cid}) 56 | 57 | # Check the view record 58 | assert caplog.records[0].correlation_id == cid 59 | assert caplog.records[0].celery_current_id is None 60 | assert caplog.records[0].celery_parent_id is None 61 | 62 | last_current_id = None 63 | 64 | for record in caplog.records[1:3]: 65 | # Check that the correlation ID is persisted 66 | assert record.correlation_id == cid 67 | 68 | # Make sure the celery current ID is a valid UUID and present 69 | assert UUID(record.celery_current_id) 70 | 71 | # Make sure the parent ID matches the previous current ID 72 | assert record.celery_parent_id == last_current_id 73 | 74 | last_current_id = record.celery_current_id 75 | 76 | 77 | async def test_worker_to_worker_to_worker(caplog, celery_session_app, celery_session_worker): 78 | """ 79 | We expect: 80 | - A correlation ID to be generated in the first worker and persisted to the final worker 81 | - The current ID of the first worker to be added as the 82 | parent ID of the second worker, and the same for 2 and 3 83 | """ 84 | caplog.set_level('DEBUG') 85 | 86 | # Trigger task 87 | task1.delay().get(timeout=10) 88 | 89 | # Save first correlation ID 90 | first_log = caplog.records[0] 91 | first_cid = first_log.correlation_id 92 | 93 | last_current_id = None 94 | 95 | for record in caplog.records: 96 | # Check that the correlation ID is persisted 97 | assert record.correlation_id == first_cid 98 | 99 | # Make sure the celery current ID is a valid UUID and present 100 | assert UUID(record.celery_current_id) 101 | 102 | # Make sure the parent ID matches the previous current ID 103 | assert record.celery_parent_id == last_current_id 104 | 105 | last_current_id = record.celery_current_id 106 | -------------------------------------------------------------------------------- /tests/test_extension_sentry.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import sentry_sdk 4 | from packaging import version 5 | 6 | from asgi_correlation_id.extensions.sentry import set_transaction_id 7 | 8 | id_value = 'test' 9 | 10 | 11 | def test_sentry_sdk_installed(mocker): 12 | """ 13 | Check that the scope.set_tag method is called when Sentry is installed. 14 | """ 15 | if version.parse(sentry_sdk.VERSION) >= version.parse('2.12.0'): 16 | set_tag_mock = Mock() 17 | scope_mock = Mock() 18 | scope_mock.set_tag = set_tag_mock 19 | mocker.patch.object(sentry_sdk, 'get_isolation_scope', return_value=scope_mock) 20 | set_transaction_id(id_value) 21 | set_tag_mock.assert_called_once_with('transaction_id', id_value) 22 | else: 23 | set_tag_mock = Mock() 24 | scope_mock = Mock() 25 | scope_mock.set_tag = set_tag_mock 26 | 27 | class MockedScope: 28 | def __enter__(self): 29 | return scope_mock 30 | 31 | def __exit__(self, exc_type, exc_val, exc_tb): 32 | pass 33 | 34 | mocker.patch.object(sentry_sdk, 'configure_scope', return_value=MockedScope()) 35 | set_transaction_id(id_value) 36 | set_tag_mock.assert_called_once_with('transaction_id', id_value) 37 | -------------------------------------------------------------------------------- /tests/test_log_filter.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | from logging import INFO, LogRecord 3 | from uuid import uuid4 4 | 5 | import pytest 6 | 7 | from asgi_correlation_id import CeleryTracingIdsFilter, CorrelationIdFilter 8 | from asgi_correlation_id.context import celery_current_id, celery_parent_id, correlation_id 9 | 10 | # Initialize context variables to obtain reset tokens which we can later use 11 | # when testing application of filter default values. 12 | correlation_id_token: contextvars.Token = correlation_id.set(None) 13 | celery_parent_id_token: contextvars.Token = celery_parent_id.set(None) 14 | celery_current_id_token: contextvars.Token = celery_current_id.set(None) 15 | 16 | 17 | @pytest.fixture 18 | def cid(): 19 | """Set and return a correlation ID""" 20 | cid = uuid4().hex 21 | correlation_id.set(cid) 22 | return cid 23 | 24 | 25 | @pytest.fixture 26 | def log_record(): 27 | """Create and return an INFO-level log record""" 28 | return LogRecord(name='', level=INFO, pathname='', lineno=0, msg='Hello, world!', args=(), exc_info=None) 29 | 30 | 31 | def test_filter_has_uuid_length_attributes(): 32 | filter_ = CorrelationIdFilter(uuid_length=8) 33 | assert filter_.uuid_length == 8 34 | 35 | 36 | def test_filter_has_default_value_attributes(): 37 | filter_ = CorrelationIdFilter(default_value='-') 38 | assert filter_.default_value == '-' 39 | 40 | 41 | def test_filter_adds_correlation_id(cid: str, log_record: LogRecord): 42 | filter_ = CorrelationIdFilter() 43 | 44 | assert not hasattr(log_record, 'correlation_id') 45 | filter_.filter(log_record) 46 | assert log_record.correlation_id == cid 47 | 48 | 49 | def test_filter_truncates_correlation_id(cid: str, log_record: LogRecord): 50 | filter_ = CorrelationIdFilter(uuid_length=8) 51 | 52 | assert not hasattr(log_record, 'correlation_id') 53 | filter_.filter(log_record) 54 | assert len(log_record.correlation_id) == 8 # Needs to match uuid_length 55 | assert cid.startswith(log_record.correlation_id) # And needs to be the first 8 characters of the id 56 | 57 | 58 | def test_filter_uses_default_value(cid: str, log_record: LogRecord): 59 | """ 60 | We expect the filter to set the log record attribute to the default value 61 | if the context variable is not set. 62 | """ 63 | filter_ = CorrelationIdFilter(default_value='-') 64 | correlation_id.reset(correlation_id_token) 65 | 66 | assert not hasattr(log_record, 'correlation_id') 67 | filter_.filter(log_record) 68 | assert log_record.correlation_id == '-' 69 | 70 | 71 | def test_celery_filter_has_uuid_length_attributes(): 72 | filter_ = CeleryTracingIdsFilter(uuid_length=8) 73 | assert filter_.uuid_length == 8 74 | 75 | 76 | def test_celery_filter_has_default_value_attributes(): 77 | filter_ = CeleryTracingIdsFilter(default_value='-') 78 | assert filter_.default_value == '-' 79 | 80 | 81 | def test_celery_filter_adds_parent_id(cid: str, log_record: LogRecord): 82 | filter_ = CeleryTracingIdsFilter() 83 | celery_parent_id.set('a') 84 | 85 | assert not hasattr(log_record, 'celery_parent_id') 86 | filter_.filter(log_record) 87 | assert log_record.celery_parent_id == 'a' 88 | 89 | 90 | def test_celery_filter_adds_current_id(cid: str, log_record: LogRecord): 91 | filter_ = CeleryTracingIdsFilter() 92 | celery_current_id.set('b') 93 | 94 | assert not hasattr(log_record, 'celery_current_id') 95 | filter_.filter(log_record) 96 | assert log_record.celery_current_id == 'b' 97 | 98 | 99 | def test_celery_filter_uses_default_value(cid: str, log_record: LogRecord): 100 | """ 101 | We expect the filter to set the log record attributes to the default value 102 | if the context variables are not not set. 103 | """ 104 | filter_ = CeleryTracingIdsFilter(default_value='-') 105 | celery_parent_id.reset(celery_parent_id_token) 106 | celery_current_id.reset(celery_current_id_token) 107 | 108 | assert not hasattr(log_record, 'celery_parent_id') 109 | assert not hasattr(log_record, 'celery_current_id') 110 | filter_.filter(log_record) 111 | assert log_record.celery_parent_id == '-' 112 | assert log_record.celery_current_id == '-' 113 | 114 | 115 | @pytest.mark.parametrize( 116 | ('uuid_length', 'expected'), 117 | [ 118 | (6, 6), 119 | (16, 16), 120 | (None, 36), 121 | (38, 36), 122 | ], 123 | ) 124 | def test_celery_filter_truncates_current_id_correctly(cid: str, log_record: LogRecord, uuid_length, expected): 125 | """ 126 | If uuid is unspecified, the default should be 36. 127 | 128 | Otherwise, the id should be truncated to the specified length. 129 | """ 130 | filter_ = CeleryTracingIdsFilter(uuid_length=uuid_length) 131 | celery_id = str(uuid4()) 132 | celery_current_id.set(celery_id) 133 | 134 | assert not hasattr(log_record, 'celery_current_id') 135 | filter_.filter(log_record) 136 | assert log_record.celery_current_id == celery_id[:expected] 137 | 138 | 139 | def test_celery_filter_maintains_current_behavior(cid: str, log_record: LogRecord): 140 | """Maintain default behavior with signature change 141 | 142 | Since the default values of CeleryTracingIdsFilter are being changed, 143 | the new default values should also not trim a hex uuid. 144 | """ 145 | celery_id = uuid4().hex 146 | celery_current_id.set(celery_id) 147 | new_filter = CeleryTracingIdsFilter() 148 | 149 | assert not hasattr(log_record, 'celery_current_id') 150 | new_filter.filter(log_record) 151 | assert log_record.celery_current_id == celery_id 152 | new_filter_record_id = log_record.celery_current_id 153 | 154 | del log_record.celery_current_id 155 | 156 | original_filter = CeleryTracingIdsFilter(uuid_length=32) 157 | assert not hasattr(log_record, 'celery_current_id') 158 | original_filter.filter(log_record) 159 | assert log_record.celery_current_id == celery_id 160 | original_filter_record_id = log_record.celery_current_id 161 | 162 | assert original_filter_record_id == new_filter_record_id 163 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import TYPE_CHECKING 3 | from uuid import uuid4 4 | 5 | import pytest 6 | from fastapi import Request, Response 7 | from httpx import AsyncClient 8 | from starlette.testclient import TestClient 9 | 10 | from asgi_correlation_id.middleware import FAILED_VALIDATION_MESSAGE, is_valid_uuid4 11 | from tests.conftest import ( 12 | TRANSFORMER_VALUE, 13 | default_app, 14 | generator_app, 15 | no_validator_or_transformer_app, 16 | transformer_app, 17 | update_request_header_app, 18 | ) 19 | 20 | if TYPE_CHECKING: 21 | from starlette.websockets import WebSocket 22 | 23 | logger = logging.getLogger('asgi_correlation_id') 24 | 25 | apps = [default_app, update_request_header_app, no_validator_or_transformer_app, transformer_app, generator_app] 26 | 27 | pytestmark = pytest.mark.asyncio 28 | 29 | 30 | @pytest.mark.parametrize('app', [default_app, no_validator_or_transformer_app, generator_app]) 31 | async def test_returned_response_headers(app): 32 | """ 33 | We expect our request id header to be returned back to us. 34 | """ 35 | 36 | @app.get('/test', status_code=200) 37 | async def test_view() -> dict: 38 | logger.debug('Test view') 39 | return {'test': 'test'} 40 | 41 | async with AsyncClient(app=app, base_url='http://test') as client: 42 | # Check we get the right headers back 43 | correlation_id = uuid4().hex 44 | response = await client.get('test', headers={'X-Request-ID': correlation_id}) 45 | assert response.headers['X-Request-ID'] == correlation_id 46 | 47 | # And do it one more time, jic 48 | second_correlation_id = uuid4().hex 49 | second_response = await client.get('test', headers={'X-Request-ID': second_correlation_id}) 50 | assert second_response.headers['X-Request-ID'] == second_correlation_id 51 | 52 | # Then try without specifying a request id 53 | third_response = await client.get('test') 54 | assert third_response.headers['X-Request-ID'] not in [correlation_id, second_correlation_id] 55 | 56 | 57 | @pytest.mark.parametrize('app', [update_request_header_app]) 58 | async def test_update_request_header(app): 59 | """ 60 | We expect the middleware to update the request header with the request ID 61 | value. 62 | """ 63 | 64 | @app.get('/test', status_code=200) 65 | async def test_view(request: Request) -> dict: 66 | logger.debug('Test view') 67 | return {'correlation_id': request.headers.get('X-Request-ID')} 68 | 69 | async with AsyncClient(app=app, base_url='http://test') as client: 70 | # Check for newly generated request ID in the request header if none 71 | # was initially provided. 72 | response = await client.get('test') 73 | assert is_valid_uuid4(response.json()['correlation_id']) 74 | 75 | # Check for newly generated request ID in the request header if it 76 | # initially contains an invalid value. 77 | response = await client.get('test', headers={'X-Request-ID': 'invalid'}) 78 | assert is_valid_uuid4(response.json()['correlation_id']) 79 | 80 | # Check for our request ID value in the request header. 81 | correlation_id = uuid4().hex 82 | response = await client.get('test', headers={'X-Request-ID': correlation_id}) 83 | assert response.json()['correlation_id'] == correlation_id 84 | 85 | 86 | bad_uuids = [ 87 | 'test', 88 | 'bad-uuid', 89 | '1x' * 16, # len of uuid is 32 90 | uuid4().hex[:-1] + 'x', 91 | ] 92 | 93 | 94 | @pytest.mark.parametrize('value', bad_uuids) 95 | @pytest.mark.parametrize('app', [default_app, transformer_app, generator_app]) 96 | async def test_non_uuid_header(caplog, value, app): 97 | """ 98 | We expect the middleware to ignore our request ID and log a warning 99 | when the request ID we pass doesn't correspond to the uuid4 format. 100 | """ 101 | 102 | @app.get('/test', status_code=200) 103 | async def test_view() -> dict: 104 | logger.debug('Test view') 105 | return {'test': 'test'} 106 | 107 | async with AsyncClient(app=app, base_url='http://test') as client: 108 | response = await client.get('test', headers={'X-Request-ID': value}) 109 | new_value = response.headers['X-Request-ID'] 110 | assert new_value != value 111 | assert caplog.messages[0] == FAILED_VALIDATION_MESSAGE.replace('%s', new_value) 112 | 113 | 114 | @pytest.mark.parametrize('app', apps) 115 | async def test_websocket_request(caplog, app): 116 | """ 117 | We expect websocket requests to not be handled. 118 | This test could use improvement. 119 | """ 120 | 121 | @app.websocket_route('/ws') 122 | async def websocket(websocket: 'WebSocket'): 123 | # Check we get the right headers back 124 | assert websocket.headers.get('x-request-id') is not None 125 | 126 | await websocket.accept() 127 | await websocket.send_json({'msg': 'Hello WebSocket'}) 128 | await websocket.close() 129 | 130 | client = TestClient(app) 131 | with client.websocket_connect('/ws') as ws: 132 | ws.receive_json() 133 | assert caplog.messages == [] 134 | 135 | 136 | @pytest.mark.parametrize('app', apps) 137 | async def test_multiple_headers_same_name(caplog, app): 138 | """ 139 | The middleware should not change the headers that were set in the response and return all of them as it is. 140 | """ 141 | 142 | @app.get('/multiple_headers_same_name') 143 | async def multiple_headers_response() -> Response: 144 | response = Response(status_code=204) 145 | response.set_cookie('access_token_cookie', 'test-access-token') 146 | response.set_cookie('refresh_token_cookie', 'test-refresh-token') 147 | return response 148 | 149 | async with AsyncClient(app=app, base_url='http://test') as client: 150 | response = await client.get('multiple_headers_same_name') 151 | assert response.headers['set-cookie'].find('access_token_cookie') != -1 152 | assert response.headers['set-cookie'].find('refresh_token_cookie') != -1 153 | 154 | 155 | async def test_no_validator(): 156 | async with AsyncClient(app=no_validator_or_transformer_app, base_url='http://test') as client: 157 | response = await client.get('test', headers={'X-Request-ID': 'bad-uuid'}) 158 | assert response.headers['X-Request-ID'] == 'bad-uuid' 159 | 160 | 161 | async def test_custom_transformer(): 162 | cid = uuid4().hex 163 | async with AsyncClient(app=transformer_app, base_url='http://test') as client: 164 | response = await client.get('test', headers={'X-Request-ID': cid}) 165 | assert response.headers['X-Request-ID'] == cid * 2 166 | 167 | 168 | async def test_custom_generator(): 169 | async with AsyncClient(app=generator_app, base_url='http://test') as client: 170 | response = await client.get('test', headers={'X-Request-ID': 'bad-uuid'}) 171 | assert response.headers['X-Request-ID'] == TRANSFORMER_VALUE 172 | 173 | 174 | def test_is_valid_uuid4(): 175 | assert is_valid_uuid4('3758c31e-1177-4540-ba33-0109c405579a') is True 176 | assert is_valid_uuid4('9e6454c4-21d5-4e4a-a66a-b28f15576414') is True 177 | assert is_valid_uuid4('9e6454c421d54e4aa66ab28f15576414') is True 178 | assert is_valid_uuid4('foo') is False 179 | assert is_valid_uuid4('9e6454c4-21d5-4e4a-a66a-b28f15576414-1') is False 180 | assert is_valid_uuid4('00000000000000000000000000000000') is False 181 | --------------------------------------------------------------------------------