├── .github ├── renovate.json5 ├── set-version └── workflows │ └── build-test-release.yml ├── DEVELOPERS.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── errors.png └── logs.png ├── examples └── cloud-function │ ├── .gcloudignore │ ├── Makefile │ ├── main.py │ └── requirements.txt ├── pyproject.toml ├── structlog_gcp ├── __about__.py ├── __init__.py ├── base.py ├── constants.py ├── error_reporting.py ├── processors.py └── py.typed ├── tests ├── __init__.py ├── conftest.py ├── fakes.py └── test_log.py └── uv.lock /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | 7 | "lockFileMaintenance": { 8 | "enabled": true, 9 | }, 10 | 11 | "schedule": ["after 6am on monday"], 12 | } 13 | -------------------------------------------------------------------------------- /.github/set-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | import argparse 5 | import io 6 | import os 7 | 8 | GIT_REF_PREFIX = "refs/tags/" 9 | 10 | 11 | def github_ref_type(value: str | None) -> str: 12 | if value is None: 13 | raise ValueError("No GITHUB_REF environment variable") 14 | 15 | if not value.startswith(GIT_REF_PREFIX): 16 | raise ValueError("Invalid GITHUB_REF format, expects `refs/tags/`") 17 | 18 | return value 19 | 20 | 21 | def write_about_file(filename: str, git_ref: str) -> str: 22 | tag = git_ref[len(GIT_REF_PREFIX) :] 23 | value = f'__version__ = "{tag}"' 24 | 25 | print(f"Writing version {tag} to {filename}") 26 | with open(filename, "w") as fp: 27 | print(value, file=fp) 28 | 29 | return tag 30 | 31 | 32 | def main() -> None: 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument( 35 | "-g", 36 | "--github-ref", 37 | default=os.environ.get("GITHUB_REF"), 38 | help=( 39 | 'The Git reference, like "refs/tags/". ' 40 | "Read automatically the GITHUB_REF environment variable, if possible." 41 | ), 42 | ) 43 | parser.add_argument( 44 | "-f", 45 | "--filename", 46 | default="structlog_gcp/__about__.py", 47 | help="The file to write the version into.", 48 | ) 49 | args = parser.parse_args() 50 | 51 | try: 52 | github_ref = github_ref_type(args.github_ref) 53 | except Exception as exc: 54 | parser.error(str(exc)) 55 | 56 | write_about_file(args.filename, github_ref) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /.github/workflows/build-test-release.yml: -------------------------------------------------------------------------------- 1 | name: Test & release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | release: 13 | types: 14 | - published 15 | 16 | workflow_dispatch: 17 | 18 | jobs: 19 | check: 20 | name: Check 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | python-version: 27 | - "3.10" 28 | - "3.11" 29 | - "3.12" 30 | - "3.13" 31 | 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Install the latest version of uv 37 | uses: astral-sh/setup-uv@v6 38 | with: 39 | enable-cache: true 40 | 41 | - name: Install Python 42 | run: uv python install ${{ matrix.python-version }} 43 | 44 | - name: Test 45 | run: make test 46 | 47 | - name: mypy 48 | run: make mypy 49 | 50 | - name: lint 51 | run: uv run ruff check 52 | 53 | - name: format 54 | run: uv run ruff format --diff 55 | 56 | # Just test the build works, we'll upload the one in the next job 57 | # instead, if needed. 58 | - name: Build 59 | run: make build 60 | 61 | build: 62 | name: Build package 63 | needs: check 64 | if: github.event.action == 'published' 65 | runs-on: ubuntu-latest 66 | 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v4 70 | 71 | - name: Install the latest version of uv 72 | uses: astral-sh/setup-uv@v6 73 | with: 74 | enable-cache: true 75 | 76 | - name: Configure version 77 | if: github.event.action == 'published' 78 | run: ./.github/set-version 79 | 80 | - name: Build 81 | run: make build 82 | 83 | - name: Upload package 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: Packages 87 | path: dist/* 88 | 89 | publish: 90 | name: Publish package 91 | needs: build 92 | runs-on: ubuntu-latest 93 | environment: pypi-release 94 | 95 | permissions: 96 | id-token: write 97 | 98 | steps: 99 | - name: Download package 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: Packages 103 | path: dist 104 | 105 | - name: Publish package distributions to PyPI 106 | uses: pypa/gh-action-pypi-publish@release/v1 107 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | # For Developers 2 | 3 | ## How to release? 4 | 5 | * Create a new GitHub Release 6 | * Publish the release 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jonathan Ballet and contributors 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: fmt mypy test 2 | 3 | .PHONY: mypy 4 | mypy: 5 | uv run mypy 6 | 7 | .PHONY: fmt 8 | fmt: 9 | uv run ruff format 10 | uv run ruff check --fix 11 | 12 | .PHONY: test 13 | test: 14 | uv run pytest 15 | 16 | .PHONY: build 17 | build: 18 | uv run python -m build --installer=uv 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Logging formatter for `structlog` 2 | 3 | This is an opiniated package that configures [structlog](https://structlog.org/) 4 | to output log compatible with the [Google Cloud Logging log 5 | format](https://cloud.google.com/logging/docs/structured-logging). 6 | 7 | The intention of this package is to be used for applications that run in [Google 8 | Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/) or [Google 9 | Cloud Function](https://cloud.google.com/functions/), or any other systems that 10 | know how to send logs to Google Cloud. 11 | 12 | As such, the package is only concerned about **formatting logs**, where logs are 13 | expected to be written on the standard output. Sending the logs to the actual 14 | Google Logging API is supposed to be done by an external agent. 15 | 16 | 17 | In particular, this package provides the following configuration by default: 18 | 19 | * Logs are formatted as JSON using the [Google Cloud Logging log format](https://cloud.google.com/logging/docs/structured-logging) 20 | * The [Python standard library's `logging`](https://docs.python.org/3/library/logging.html) 21 | log levels are available and translated to their GCP equivalents. 22 | * Exceptions and `CRITICAL` log messages will be reported into [Google Error Reporting dashboard](https://cloud.google.com/error-reporting/) 23 | * Additional logger bound arguments will be reported into the `jsonPayload` event. 24 | 25 | 26 | ## How to use? 27 | 28 | Install the package with `pip` or your favorite Python package manager: 29 | 30 | ```sh 31 | pip install structlog-gcp 32 | ``` 33 | 34 | Then, configure `structlog` as usual, using the Structlog processors the package 35 | provides: 36 | 37 | ```python 38 | import structlog 39 | import structlog_gcp 40 | 41 | processors = structlog_gcp.build_processors() 42 | structlog.configure(processors=processors) 43 | ``` 44 | 45 | Then, you can use `structlog` as usual: 46 | 47 | ```python 48 | logger = structlog.get_logger().bind(arg1="something") 49 | 50 | logger.info("Hello world") 51 | 52 | converted = False 53 | try: 54 | int("foobar") 55 | converted = True 56 | except: 57 | logger.exception("Something bad happens") 58 | 59 | if not converted: 60 | logger.critical("This is not supposed to happen", converted=converted) 61 | 62 | try: 63 | 1 / 0 64 | except ZeroDivisionError as exc: 65 | logger.info("This was known to happen! {exc}") 66 | ``` 67 | 68 | The `structlog_gcp.build_processors()` function constructs structlog processors to: 69 | 70 | * Output logs as Google Cloud Logging format using the default Python JSON serializer. 71 | * Carry context variables across loggers (see [structlog: Context Variables](https://www.structlog.org/en/stable/contextvars.html)) 72 | 73 | For more advanced usage, see [Advanced Configuration](#advanced-configuration) 74 | 75 | 76 | ### Errors 77 | 78 | 79 | Errors are automatically reported to the [Google Error Reporting service](https://cloud.google.com/error-reporting/), most of the time. 80 | 81 | #### Using `logger.exception` 82 | 83 | Using: 84 | 85 | ```python 86 | try: 87 | 1 / 0 88 | except: 89 | logger.exception("oh no") 90 | ``` 91 | 92 | Will give you: 93 | 94 | * The current exception can automatically added into the log event 95 | * The log level will be `ERROR` 96 | * The exception will be reported in Error Reporting 97 | 98 | ##### Using `logger.$LEVEL(..., exception=exc)` 99 | 100 | Using: 101 | 102 | ```python 103 | try: 104 | 1 / 0 105 | except Exception as exc 106 | logger.info("oh no", exception=exc) 107 | ``` 108 | Will give you: 109 | 110 | * The specified exception will be part of the log event 111 | * The log level will be `INFO`, or whichever log level you used 112 | * The exception will be reported in Error Reporting 113 | 114 | 115 | ##### Using `logger.$LEVEL(...)` 116 | 117 | Not passing any `exception` argument to the logger, as in: 118 | 119 | ```python 120 | try: 121 | 1 / 0 122 | except Exception as exc 123 | logger.warning(f"oh no: {exc}") 124 | ``` 125 | 126 | Will give you: 127 | 128 | * The exception will **not** be part of the log event. 129 | * The log level will be `WARNING` (or whichever log level you used) 130 | * AND the exception will **not** be reported in Error Reporting 131 | 132 | ### Configuration 133 | 134 | You can configure the service name and the version used during the report with 2 different ways: 135 | 136 | * By default, the library assumes to run with Cloud Run environment 137 | variables configured, in particular [the `K_SERVICE` and `K_REVISION` variables](https://cloud.google.com/run/docs/configuring/services/overview-environment-variables#reserved_environment_variables_for_functions). 138 | * You can also pass the service name and revision at configuration time with: 139 | 140 | ```python 141 | import structlog 142 | import structlog_gcp 143 | 144 | processors = structlog_gcp.build_processors( 145 | service="my-service", 146 | version="v1.2.3", 147 | ) 148 | structlog.configure(processors=processors) 149 | ``` 150 | 151 | ### Advanced Configuration 152 | 153 | If you need to have more control over the processors configured by the library, you can use the `structlog_gcp.build_gcp_processors()` builder function. 154 | 155 | This function only configures the Google Cloud Logging-specific processors and omits all the rest. 156 | 157 | In particular, you can use this function: 158 | 159 | * If you want to have more control over the processors to be configured in structlog. You can prepend or append other processors around the Google-specific ones. 160 | * If you want to serialize using another JSON serializer or with specific options. 161 | 162 | For instance: 163 | 164 | 165 | ```python 166 | import orjson 167 | import structlog 168 | from structlog.processors import JSONRenderer 169 | 170 | import structlog_gcp 171 | 172 | 173 | def add_open_telemetry_spans(...): 174 | # Cf. https://www.structlog.org/en/stable/frameworks.html#opentelemetry 175 | ... 176 | 177 | gcp_processors = structlog_gcp.build_gcp_processors() 178 | 179 | # Fine-tune processors 180 | processors = [add_open_telemetry_spans] 181 | processors.extend(gcp_processors) 182 | processors.append(JSONRenderer(serializer=orjson.dumps)) 183 | 184 | structlog.configure(processors=processors) 185 | ``` 186 | 187 | > [!IMPORTANT] 188 | > 189 | > `structlog_gcp.build_gcp_processors()` **doesn't** configure a renderer and 190 | > you must supply a JSON renderer of your choice for the library to work 191 | > correctly. 192 | 193 | 194 | ## Examples 195 | 196 | Check out the [`examples` folder](https://github.com/multani/structlog-gcp/tree/main/examples) to see how it can be used. 197 | 198 | * How it should appear in the Google Cloud Logging log explorer: 199 | ![](https://raw.githubusercontent.com/multani/structlog-gcp/main/docs/logs.png) 200 | 201 | * How it should appear in the Google Cloud Error Reporting dashboard: 202 | ![](https://raw.githubusercontent.com/multani/structlog-gcp/main/docs/errors.png) 203 | 204 | 205 | ## Reference 206 | 207 | * https://cloud.google.com/logging/docs/structured-logging 208 | * https://cloud.google.com/error-reporting/docs/formatting-error-messages 209 | * https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry 210 | -------------------------------------------------------------------------------- /docs/errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multani/structlog-gcp/28adade9ac7eb7b474b5571f91907d6a09d3516e/docs/errors.png -------------------------------------------------------------------------------- /docs/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multani/structlog-gcp/28adade9ac7eb7b474b5571f91907d6a09d3516e/docs/logs.png -------------------------------------------------------------------------------- /examples/cloud-function/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | node_modules 17 | -------------------------------------------------------------------------------- /examples/cloud-function/Makefile: -------------------------------------------------------------------------------- 1 | deploy: 2 | gcloud functions deploy test-log \ 3 | --gen2 \ 4 | --region europe-west1 \ 5 | --runtime python310 \ 6 | --source . \ 7 | --entry-point test_func1 \ 8 | --trigger-http 9 | -------------------------------------------------------------------------------- /examples/cloud-function/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import functions_framework 4 | import google.cloud.error_reporting 5 | import google.cloud.logging 6 | import structlog 7 | 8 | import structlog_gcp 9 | 10 | processors = structlog_gcp.build_processors() 11 | structlog.configure(processors=processors) 12 | 13 | 14 | @functions_framework.http 15 | def test_func1(request): 16 | """Test the logging framework. 17 | 18 | * `GET` the deployed URL to trigger the `structlog-gcp` behavior 19 | * `POST` the deployed URL to trigger the official Google logging + error 20 | reporting libraries behavior 21 | * `DELETE` the deployed URL to crash the function and force a cold-restart 22 | """ 23 | 24 | if request.method == "GET": 25 | logger = structlog.get_logger("test") 26 | 27 | logger.debug("a debug message", foo="bar") 28 | logger.info("an info message", foo="bar") 29 | logger.warning("a warning message", arg="something else") 30 | 31 | logger.error("an error message") 32 | logger.critical("a critical message with reported error") 33 | 34 | try: 35 | 1 / 0 36 | except ZeroDivisionError: 37 | logger.exception("division by zero") 38 | 39 | try: 40 | raise TypeError("crash") 41 | except TypeError: 42 | logger.exception("type error") 43 | 44 | elif request.method == "POST": 45 | error = google.cloud.error_reporting.Client() 46 | google.cloud.logging.Client().setup_logging() 47 | 48 | logging.debug("a debug message") 49 | logging.info("an info message") 50 | logging.warning("a warning message") 51 | 52 | logging.error("an error message") 53 | logging.critical("a critical message with reported error") 54 | 55 | error.report("a reported error") 56 | 57 | try: 58 | 1 / 0 59 | except ZeroDivisionError: 60 | error.report_exception() 61 | 62 | try: 63 | raise TypeError("crash") 64 | except TypeError: 65 | logging.exception("type error") 66 | 67 | elif request.method == "DELETE": 68 | # crash the function to force a cold restart 69 | raise RuntimeError("restart") 70 | 71 | return "OK" 72 | -------------------------------------------------------------------------------- /examples/cloud-function/requirements.txt: -------------------------------------------------------------------------------- 1 | functions-framework==3.* 2 | google-cloud-error-reporting==1.* 3 | structlog-gcp 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "structlog-gcp" 3 | description = "A structlog set of processors to output as Google Cloud Logging format" 4 | readme = "README.md" 5 | requires-python = ">=3.10" 6 | license = { file = "LICENSE" } 7 | keywords = [] 8 | authors = [ 9 | { name = "Jonathan Ballet", email = "jon@multani.info" }, 10 | ] 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Programming Language :: Python", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13", 18 | "Programming Language :: Python :: Implementation :: CPython", 19 | "Programming Language :: Python :: Implementation :: PyPy", 20 | ] 21 | dependencies = [ 22 | "structlog", 23 | ] 24 | dynamic = ["version"] 25 | 26 | [project.urls] 27 | Documentation = "https://github.com/multani/structlog-gcp#readme" 28 | Issues = "https://github.com/multani/structlog-gcp/issues" 29 | Source = "https://github.com/multani/structlog-gcp" 30 | 31 | [build-system] 32 | requires = ["hatchling"] 33 | build-backend = "hatchling.build" 34 | 35 | [tool.hatch.version] 36 | path = "structlog_gcp/__about__.py" 37 | 38 | [tool.uv] 39 | dev-dependencies = [ 40 | "build[uv]>=1.2.2", 41 | "mypy>=1.11.2", 42 | "pytest>=8.3.3", 43 | "pytest-cov>=5.0.0", 44 | "ruff>=0.6.6", 45 | ] 46 | 47 | [tool.pytest.ini_options] 48 | addopts = [ 49 | "--doctest-modules", 50 | "--cov", "structlog_gcp", 51 | "--cov-branch", 52 | "--cov-report", "html", 53 | "--cov-report", "term", 54 | "--tb", "short", 55 | "--verbose", 56 | "--verbose", 57 | ] 58 | testpaths = "tests" 59 | 60 | [tool.coverage.run] 61 | branch = true 62 | parallel = true 63 | omit = [ 64 | "structlog_gcp/__about__.py", 65 | ] 66 | 67 | [tool.mypy] 68 | strict = true 69 | files = ["structlog_gcp", "tests"] 70 | 71 | [tool.ruff.lint] 72 | extend-select = ["I"] 73 | -------------------------------------------------------------------------------- /structlog_gcp/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1.dev" # Will be overwritten during deployment 2 | -------------------------------------------------------------------------------- /structlog_gcp/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import build_gcp_processors, build_processors # noqa: F401 2 | 3 | __all__ = [ 4 | "build_gcp_processors", 5 | "build_processors", 6 | ] 7 | -------------------------------------------------------------------------------- /structlog_gcp/base.py: -------------------------------------------------------------------------------- 1 | import structlog.contextvars 2 | import structlog.processors 3 | from structlog.typing import Processor 4 | 5 | from . import error_reporting, processors 6 | 7 | 8 | def build_processors( 9 | service: str | None = None, 10 | version: str | None = None, 11 | ) -> list[Processor]: 12 | """Build structlog processors to export logs for Google Cloud Logging. 13 | 14 | This builder function is expected to be your go-to function to expose structlog logs to Google 15 | Cloud Logging format. 16 | 17 | It configures the Google Cloud Logging-specific processors but also good defaults processors. 18 | 19 | If you need more control over which processors are exactly configured, check the 20 | :ref:`build_gcp_processors` function. 21 | """ 22 | 23 | procs: list[Processor] = [] 24 | 25 | procs.append(structlog.contextvars.merge_contextvars) 26 | procs.extend(build_gcp_processors(service, version)) 27 | procs.append(structlog.processors.JSONRenderer()) 28 | 29 | return procs 30 | 31 | 32 | def build_gcp_processors( 33 | service: str | None = None, 34 | version: str | None = None, 35 | ) -> list[Processor]: 36 | """Build only the Google Cloud Logging-specific processors. 37 | 38 | This builds a set of processors to format logs according to what Google Cloud Logging expects. 39 | 40 | See: https://cloud.google.com/functions/docs/monitoring/logging#writing_structured_logs 41 | 42 | Use this builder function if you want to customize the processors before and after the 43 | GCP-specific processors. 44 | 45 | In particular, this builder function **doesn't** configure the final JSON renderer. You are 46 | expected to provide your own. 47 | 48 | For a simpler, more general alternative, use :ref:`build_processors` instead. 49 | """ 50 | 51 | procs: list[Processor] = [] 52 | 53 | # Add a timestamp in ISO 8601 format. 54 | procs.append(structlog.processors.TimeStamper(fmt="iso")) 55 | procs.append(processors.init_cloud_logging) 56 | 57 | procs.append(processors.LogSeverity()) 58 | procs.extend(processors.setup_code_location()) 59 | 60 | # Errors: log exceptions 61 | procs.extend(error_reporting.setup_exceptions()) 62 | 63 | # Errors: formatter for Error Reporting 64 | procs.append(error_reporting.ReportError(["CRITICAL"])) 65 | 66 | # Errors: add service context 67 | procs.append(error_reporting.ServiceContext(service, version)) 68 | 69 | # Finally: Cloud Logging formatter 70 | procs.append(processors.finalize_cloud_logging) 71 | 72 | return procs 73 | -------------------------------------------------------------------------------- /structlog_gcp/constants.py: -------------------------------------------------------------------------------- 1 | ERROR_EVENT_TYPE = ( 2 | "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent" 3 | ) 4 | 5 | SOURCE_LOCATION_KEY = "logging.googleapis.com/sourceLocation" 6 | 7 | CLOUD_LOGGING_KEY = "cloud-logging" 8 | 9 | # From Python's logging level to Google level 10 | # https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity 11 | SEVERITY_MAPPING = { 12 | "notset": "DEFAULT", # The log entry has no assigned severity level. 13 | "debug": "DEBUG", # Debug or trace information. 14 | "info": "INFO", # Routine information, such as ongoing status or performance. 15 | # "notice": "NOTICE", # Normal but significant events, such as start up, shut down, or a configuration change. 16 | "warn": "WARNING", # Warning events might cause problems. 17 | "warning": "WARNING", # Warning events might cause problems. 18 | "error": "ERROR", # Error events are likely to cause problems. 19 | "critical": "CRITICAL", # Critical events cause more severe problems or outages. 20 | # "alert": "ALERT", # A person must take an action immediately. 21 | # "emergency": "EMERGENCY", # One or more systems are unusable. 22 | } 23 | -------------------------------------------------------------------------------- /structlog_gcp/error_reporting.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import structlog.processors 4 | from structlog.typing import EventDict, Processor, WrappedLogger 5 | 6 | from .constants import CLOUD_LOGGING_KEY, ERROR_EVENT_TYPE, SOURCE_LOCATION_KEY 7 | 8 | 9 | def setup_exceptions() -> list[Processor]: 10 | return [structlog.processors.format_exc_info, ReportException()] 11 | 12 | 13 | class ReportException: 14 | """Transform exception into a Google Cloud Error Reporting event.""" 15 | 16 | # https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report 17 | # https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-entry-examples 18 | 19 | def __init__(self) -> None: 20 | pass 21 | 22 | def __call__( 23 | self, logger: WrappedLogger, method_name: str, event_dict: EventDict 24 | ) -> EventDict: 25 | exception = event_dict.pop("exception", None) 26 | if exception is None: 27 | return event_dict 28 | 29 | event_dict[CLOUD_LOGGING_KEY]["@type"] = ERROR_EVENT_TYPE 30 | 31 | # https://cloud.google.com/error-reporting/docs/formatting-error-messages 32 | message = event_dict[CLOUD_LOGGING_KEY]["message"] 33 | error_message = f"{message}\n{exception}" 34 | event_dict[CLOUD_LOGGING_KEY]["stack_trace"] = error_message 35 | 36 | return event_dict 37 | 38 | 39 | class ReportError: 40 | """Report to Google Cloud Error Reporting specific log severities 41 | 42 | This class assumes the :ref:`.processors.CodeLocation` processor ran before. 43 | """ 44 | 45 | # https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report 46 | # https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-entry-examples 47 | 48 | def __init__(self, severities: list[str]) -> None: 49 | self.severities = severities 50 | 51 | def __call__( 52 | self, logger: WrappedLogger, method_name: str, event_dict: EventDict 53 | ) -> EventDict: 54 | severity = event_dict[CLOUD_LOGGING_KEY]["severity"] 55 | has_strack_trace = "stack_trace" in event_dict[CLOUD_LOGGING_KEY] 56 | 57 | if severity not in self.severities and not has_strack_trace: 58 | return event_dict 59 | 60 | # https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorContext 61 | error_context = { 62 | "reportLocation": event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY], 63 | } 64 | 65 | event_dict[CLOUD_LOGGING_KEY]["@type"] = ERROR_EVENT_TYPE 66 | event_dict[CLOUD_LOGGING_KEY]["context"] = error_context 67 | 68 | # "serviceContext" should be added by the ServiceContext processor. 69 | # event_dict[CLOUD_LOGGING_KEY]["serviceContext"] 70 | 71 | return event_dict 72 | 73 | 74 | class ServiceContext: 75 | def __init__(self, service: str | None = None, version: str | None = None) -> None: 76 | # https://cloud.google.com/functions/docs/configuring/env-var#runtime_environment_variables_set_automatically 77 | if service is None: 78 | service = os.environ.get("K_SERVICE", "unknown service") 79 | 80 | if version is None: 81 | version = os.environ.get("K_REVISION", "unknown version") 82 | 83 | self.service_context = {"service": service, "version": version} 84 | 85 | def __call__( 86 | self, logger: WrappedLogger, method_name: str, event_dict: EventDict 87 | ) -> EventDict: 88 | """Add a service context in which an error has occurred. 89 | 90 | This is part of the Error Reporting API, so it's only added when an error happens. 91 | """ 92 | 93 | event_type = event_dict[CLOUD_LOGGING_KEY].get("@type") 94 | if event_type != ERROR_EVENT_TYPE: 95 | return event_dict 96 | 97 | # https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext 98 | event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = self.service_context 99 | 100 | return event_dict 101 | -------------------------------------------------------------------------------- /structlog_gcp/processors.py: -------------------------------------------------------------------------------- 1 | # https://cloud.google.com/functions/docs/monitoring/logging#writing_structured_logs 2 | # https://cloud.google.com/logging/docs/agent/logging/configuration#process-payload 3 | # https://cloud.google.com/logging/docs/structured-logging#special-payload-fields 4 | 5 | 6 | import structlog.processors 7 | from structlog.typing import EventDict, Processor, WrappedLogger 8 | 9 | from .constants import CLOUD_LOGGING_KEY, SEVERITY_MAPPING, SOURCE_LOCATION_KEY 10 | 11 | 12 | def setup_code_location() -> list[Processor]: 13 | call_site_processors = structlog.processors.CallsiteParameterAdder( 14 | parameters=[ 15 | structlog.processors.CallsiteParameter.PATHNAME, 16 | structlog.processors.CallsiteParameter.MODULE, 17 | structlog.processors.CallsiteParameter.FUNC_NAME, 18 | structlog.processors.CallsiteParameter.LINENO, 19 | ] 20 | ) 21 | 22 | return [call_site_processors, code_location] 23 | 24 | 25 | def init_cloud_logging( 26 | logger: WrappedLogger, method_name: str, event_dict: EventDict 27 | ) -> EventDict: 28 | """Initialize the Google Cloud Logging event message""" 29 | 30 | value = { 31 | "message": event_dict.pop("event"), 32 | "time": event_dict.pop("timestamp"), 33 | } 34 | 35 | event_dict[CLOUD_LOGGING_KEY] = value 36 | return event_dict 37 | 38 | 39 | def finalize_cloud_logging( 40 | logger: WrappedLogger, method_name: str, event_dict: EventDict 41 | ) -> EventDict: 42 | """Finalize the Google Cloud Logging event message and replace the logging event. 43 | 44 | This is not exactly the format the Cloud Logging directly ingests, but 45 | Cloud Logging is smart enough to transform basic JSON-like logging events 46 | into Cloud Logging-compatible events. 47 | 48 | See: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields 49 | """ 50 | 51 | # Take out the Google Cloud Logging set of fields from the event dict 52 | gcp_event: EventDict = event_dict.pop(CLOUD_LOGGING_KEY) 53 | 54 | # Override whatever is left from the event dict with the content of all 55 | # the Google Cloud Logging-formatted fields. 56 | event_dict.update(gcp_event) 57 | 58 | # Fields which are not known by Google Cloud Logging will be added to 59 | # the `jsonPayload` field. 60 | # 61 | # See the `message` field documentation in: 62 | # https://cloud.google.com/logging/docs/structured-logging#special-payload-fields 63 | 64 | return event_dict 65 | 66 | 67 | class LogSeverity: 68 | """Set the severity using the Google Cloud Logging severities. 69 | 70 | 71 | See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity 72 | """ 73 | 74 | def __init__(self) -> None: 75 | self.default = "notset" 76 | self.mapping = SEVERITY_MAPPING.copy() 77 | 78 | def __call__( 79 | self, logger: WrappedLogger, method_name: str, event_dict: EventDict 80 | ) -> EventDict: 81 | """Format a Python log level value as a GCP log severity.""" 82 | 83 | log_level = method_name 84 | severity = self.mapping.get(log_level, self.default) 85 | 86 | event_dict[CLOUD_LOGGING_KEY]["severity"] = severity 87 | return event_dict 88 | 89 | 90 | def code_location( 91 | logger: WrappedLogger, method_name: str, event_dict: EventDict 92 | ) -> EventDict: 93 | """Inject the location of the logging message into the logs""" 94 | 95 | location = { 96 | "file": event_dict.pop("pathname"), 97 | "line": str(event_dict.pop("lineno")), 98 | "function": f"{event_dict.pop('module')}:{event_dict.pop('func_name')}", 99 | } 100 | 101 | event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location 102 | 103 | return event_dict 104 | -------------------------------------------------------------------------------- /structlog_gcp/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multani/structlog-gcp/28adade9ac7eb7b474b5571f91907d6a09d3516e/structlog_gcp/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multani/structlog-gcp/28adade9ac7eb7b474b5571f91907d6a09d3516e/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Generator, Iterator 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | import structlog 7 | from _pytest.capture import CaptureFixture 8 | from structlog.typing import WrappedLogger 9 | 10 | import structlog_gcp 11 | 12 | from . import fakes 13 | 14 | Event = dict[str, Any] 15 | T_stdout = Iterator[Event] 16 | 17 | 18 | @pytest.fixture 19 | def mock_logger_env() -> Generator[None, None, None]: 20 | with ( 21 | patch( 22 | "structlog.processors.CallsiteParameterAdder", 23 | side_effect=fakes.CallsiteParameterAdder, 24 | ), 25 | patch("structlog.processors.TimeStamper", side_effect=fakes.TimeStamper), 26 | patch( 27 | "structlog.processors.format_exc_info", side_effect=fakes.format_exc_info 28 | ), 29 | ): 30 | yield 31 | 32 | 33 | @pytest.fixture 34 | def logger(mock_logger_env: None) -> Generator[WrappedLogger, None, None]: 35 | """Setup a logger for testing and return it""" 36 | 37 | structlog.reset_defaults() 38 | structlog.contextvars.clear_contextvars() 39 | 40 | processors = structlog_gcp.build_processors() 41 | structlog.configure(processors=processors) 42 | logger = structlog.get_logger() 43 | 44 | yield logger 45 | 46 | structlog.reset_defaults() 47 | 48 | 49 | @pytest.fixture 50 | def stdout(capsys: CaptureFixture[str]) -> T_stdout: 51 | def read() -> Iterator[Event]: 52 | output = capsys.readouterr() 53 | assert "" == output.err 54 | 55 | for line in output.out.split("\n"): 56 | yield json.loads(line) 57 | 58 | return read() 59 | -------------------------------------------------------------------------------- /tests/fakes.py: -------------------------------------------------------------------------------- 1 | """Fake implementations of structlog processors with side-effects""" 2 | 3 | from typing import Collection 4 | 5 | from structlog.processors import CallsiteParameter 6 | from structlog.typing import EventDict, WrappedLogger 7 | 8 | 9 | class CallsiteParameterAdder: 10 | def __init__(self, parameters: Collection[CallsiteParameter]) -> None: 11 | pass 12 | 13 | def __call__( 14 | self, logger: WrappedLogger, method_name: str, event_dict: EventDict 15 | ) -> EventDict: 16 | event_dict["pathname"] = "/app/test.py" 17 | event_dict["lineno"] = 42 18 | event_dict["module"] = "test" 19 | event_dict["func_name"] = "test123" 20 | return event_dict 21 | 22 | 23 | class TimeStamper: 24 | def __init__(self, fmt: str) -> None: 25 | pass 26 | 27 | def __call__( 28 | self, logger: WrappedLogger, method_name: str, event_dict: EventDict 29 | ) -> EventDict: 30 | event_dict["timestamp"] = "2023-04-01T08:00:00.000000Z" 31 | return event_dict 32 | 33 | 34 | def format_exc_info( 35 | logger: WrappedLogger, method_name: str, event_dict: EventDict 36 | ) -> EventDict: 37 | exc_info = event_dict.pop("exc_info", None) 38 | if exc_info: 39 | event_dict["exception"] = "Traceback blabla" 40 | return event_dict 41 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from unittest.mock import patch 3 | 4 | import structlog 5 | from _pytest.capture import CaptureFixture 6 | from structlog.typing import WrappedLogger 7 | 8 | import structlog_gcp 9 | 10 | from .conftest import T_stdout 11 | 12 | 13 | def test_normal(stdout: T_stdout, logger: WrappedLogger) -> None: 14 | logger.info("test") 15 | 16 | msg = next(stdout) 17 | 18 | expected = { 19 | "logging.googleapis.com/sourceLocation": { 20 | "file": "/app/test.py", 21 | "function": "test:test123", 22 | "line": "42", 23 | }, 24 | "message": "test", 25 | "severity": "INFO", 26 | "time": "2023-04-01T08:00:00.000000Z", 27 | } 28 | assert msg == expected 29 | 30 | 31 | def test_exception(stdout: T_stdout, logger: WrappedLogger) -> None: 32 | try: 33 | 1 / 0 34 | except ZeroDivisionError: 35 | logger.exception("oh noes", foo="bar") 36 | 37 | msg = next(stdout) 38 | 39 | expected = { 40 | "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent", 41 | "context": { 42 | "reportLocation": { 43 | "file": "/app/test.py", 44 | "function": "test:test123", 45 | "line": "42", 46 | }, 47 | }, 48 | "logging.googleapis.com/sourceLocation": { 49 | "file": "/app/test.py", 50 | "function": "test:test123", 51 | "line": "42", 52 | }, 53 | "serviceContext": { 54 | "service": "unknown service", 55 | "version": "unknown version", 56 | }, 57 | "foo": "bar", 58 | "severity": "ERROR", 59 | "message": "oh noes", 60 | "stack_trace": "oh noes\nTraceback blabla", 61 | "time": "2023-04-01T08:00:00.000000Z", 62 | } 63 | assert msg == expected 64 | 65 | 66 | def test_service_context_default(stdout: T_stdout, logger: WrappedLogger) -> None: 67 | try: 68 | 1 / 0 69 | except ZeroDivisionError: 70 | logger.exception("oh noes") 71 | 72 | msg = next(stdout) 73 | 74 | assert msg["serviceContext"] == { 75 | "service": "unknown service", 76 | "version": "unknown version", 77 | } 78 | 79 | 80 | @patch.dict("os.environ", {"K_SERVICE": "test-service", "K_REVISION": "test-version"}) 81 | def test_service_context_envvar(stdout: T_stdout, mock_logger_env: None) -> None: 82 | processors = structlog_gcp.build_processors() 83 | structlog.configure(processors=processors) 84 | logger = structlog.get_logger() 85 | 86 | try: 87 | 1 / 0 88 | except ZeroDivisionError: 89 | logger.exception("oh noes") 90 | 91 | msg = next(stdout) 92 | 93 | assert msg["serviceContext"] == { 94 | "service": "test-service", 95 | "version": "test-version", 96 | } 97 | 98 | 99 | def test_service_context_custom(stdout: T_stdout, mock_logger_env: None) -> None: 100 | processors = structlog_gcp.build_processors( 101 | service="my-service", 102 | version="deadbeef", 103 | ) 104 | structlog.configure(processors=processors) 105 | logger = structlog.get_logger() 106 | 107 | try: 108 | 1 / 0 109 | except ZeroDivisionError: 110 | logger.exception("oh noes") 111 | 112 | msg = next(stdout) 113 | 114 | assert msg["serviceContext"] == { 115 | "service": "my-service", 116 | "version": "deadbeef", 117 | } 118 | 119 | 120 | def test_extra_labels(stdout: T_stdout, logger: WrappedLogger) -> None: 121 | logger.info( 122 | "test", 123 | test1="test1", 124 | test2=2, 125 | test3=False, 126 | test4={"foo": "bar"}, 127 | test5={"date": datetime.date(2023, 1, 1)}, 128 | ) 129 | 130 | msg = next(stdout) 131 | 132 | expected = { 133 | "logging.googleapis.com/sourceLocation": { 134 | "file": "/app/test.py", 135 | "function": "test:test123", 136 | "line": "42", 137 | }, 138 | "severity": "INFO", 139 | "time": "2023-04-01T08:00:00.000000Z", 140 | "message": "test", 141 | # This should be parsed automatically by Cloud Logging into dedicated keys and saved into a JSON payload. 142 | # See: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields 143 | "test1": "test1", 144 | "test2": 2, 145 | "test3": False, 146 | "test4": {"foo": "bar"}, 147 | "test5": {"date": "datetime.date(2023, 1, 1)"}, 148 | } 149 | assert msg == expected 150 | 151 | 152 | def test_contextvars_supported(stdout: T_stdout, logger: WrappedLogger) -> None: 153 | structlog.contextvars.bind_contextvars( 154 | request_id="1234", 155 | ) 156 | 157 | logger.info("test") 158 | msg = next(stdout) 159 | 160 | expected = { 161 | "logging.googleapis.com/sourceLocation": { 162 | "file": "/app/test.py", 163 | "function": "test:test123", 164 | "line": "42", 165 | }, 166 | "message": "test", 167 | "request_id": "1234", 168 | "severity": "INFO", 169 | "time": "2023-04-01T08:00:00.000000Z", 170 | } 171 | assert msg == expected 172 | 173 | 174 | def test_core_processors_only( 175 | capsys: CaptureFixture[str], mock_logger_env: None 176 | ) -> None: 177 | processors = structlog_gcp.build_gcp_processors() 178 | processors.append(structlog.processors.KeyValueRenderer()) 179 | 180 | structlog.configure(processors=processors) 181 | logger = structlog.get_logger() 182 | 183 | # This will not be logged as the contextvars processor is not configured in the "core" processors. 184 | structlog.contextvars.bind_contextvars( 185 | request_id="1234", 186 | ) 187 | 188 | logger.info("test") 189 | 190 | output = capsys.readouterr() 191 | assert "" == output.err 192 | msg = output.out.strip() 193 | 194 | # No JSON formmating, no contextvars 195 | expected = "message='test' time='2023-04-01T08:00:00.000000Z' severity='INFO' logging.googleapis.com/sourceLocation={'file': '/app/test.py', 'line': '42', 'function': 'test:test123'}" 196 | 197 | assert expected == msg 198 | 199 | 200 | def test_exception_different_level(stdout: T_stdout, logger: WrappedLogger) -> None: 201 | try: 202 | 1 / 0 203 | except ZeroDivisionError as exc: 204 | logger.warning("oh no; anyways", exception=exc) 205 | 206 | msg = next(stdout) 207 | 208 | expected = { 209 | "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent", 210 | "context": { 211 | "reportLocation": { 212 | "file": "/app/test.py", 213 | "function": "test:test123", 214 | "line": "42", 215 | }, 216 | }, 217 | "logging.googleapis.com/sourceLocation": { 218 | "file": "/app/test.py", 219 | "function": "test:test123", 220 | "line": "42", 221 | }, 222 | "serviceContext": { 223 | "service": "unknown service", 224 | "version": "unknown version", 225 | }, 226 | "severity": "WARNING", 227 | "message": "oh no; anyways", 228 | "stack_trace": "oh no; anyways\ndivision by zero", 229 | "time": "2023-04-01T08:00:00.000000Z", 230 | } 231 | assert msg == expected 232 | 233 | 234 | def test_exception_handled(stdout: T_stdout, logger: WrappedLogger) -> None: 235 | try: 236 | 1 / 0 237 | except ZeroDivisionError as exc: 238 | logger.info(f"I was expecting that error: {exc}") 239 | 240 | msg = next(stdout) 241 | 242 | expected = { 243 | "logging.googleapis.com/sourceLocation": { 244 | "file": "/app/test.py", 245 | "function": "test:test123", 246 | "line": "42", 247 | }, 248 | "severity": "INFO", 249 | "message": "I was expecting that error: division by zero", 250 | "time": "2023-04-01T08:00:00.000000Z", 251 | } 252 | assert msg == expected 253 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "build" 7 | version = "1.2.2.post1" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "colorama", marker = "os_name == 'nt'" }, 11 | { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, 12 | { name = "packaging" }, 13 | { name = "pyproject-hooks" }, 14 | { name = "tomli", marker = "python_full_version < '3.11'" }, 15 | ] 16 | sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } 17 | wheels = [ 18 | { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, 19 | ] 20 | 21 | [package.optional-dependencies] 22 | uv = [ 23 | { name = "uv" }, 24 | ] 25 | 26 | [[package]] 27 | name = "colorama" 28 | version = "0.4.6" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 33 | ] 34 | 35 | [[package]] 36 | name = "coverage" 37 | version = "7.8.2" 38 | source = { registry = "https://pypi.org/simple" } 39 | sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" }, 42 | { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" }, 43 | { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" }, 44 | { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" }, 45 | { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" }, 46 | { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" }, 47 | { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" }, 48 | { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" }, 49 | { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" }, 50 | { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" }, 51 | { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, 52 | { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, 53 | { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, 54 | { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, 55 | { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, 56 | { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, 57 | { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, 58 | { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, 59 | { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, 60 | { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, 61 | { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, 62 | { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, 63 | { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, 64 | { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, 65 | { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, 66 | { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, 67 | { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, 68 | { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, 69 | { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, 70 | { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, 71 | { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, 72 | { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, 73 | { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, 74 | { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, 75 | { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, 76 | { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, 77 | { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, 78 | { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, 79 | { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, 80 | { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, 81 | { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, 82 | { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, 83 | { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, 84 | { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, 85 | { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, 86 | { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, 87 | { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, 88 | { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, 89 | { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, 90 | { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, 91 | { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, 92 | { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, 93 | { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, 94 | { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, 95 | { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, 96 | { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, 97 | ] 98 | 99 | [package.optional-dependencies] 100 | toml = [ 101 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 102 | ] 103 | 104 | [[package]] 105 | name = "exceptiongroup" 106 | version = "1.3.0" 107 | source = { registry = "https://pypi.org/simple" } 108 | dependencies = [ 109 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 110 | ] 111 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 112 | wheels = [ 113 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 114 | ] 115 | 116 | [[package]] 117 | name = "importlib-metadata" 118 | version = "8.7.0" 119 | source = { registry = "https://pypi.org/simple" } 120 | dependencies = [ 121 | { name = "zipp" }, 122 | ] 123 | sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } 124 | wheels = [ 125 | { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, 126 | ] 127 | 128 | [[package]] 129 | name = "iniconfig" 130 | version = "2.1.0" 131 | source = { registry = "https://pypi.org/simple" } 132 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 133 | wheels = [ 134 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 135 | ] 136 | 137 | [[package]] 138 | name = "mypy" 139 | version = "1.16.0" 140 | source = { registry = "https://pypi.org/simple" } 141 | dependencies = [ 142 | { name = "mypy-extensions" }, 143 | { name = "pathspec" }, 144 | { name = "tomli", marker = "python_full_version < '3.11'" }, 145 | { name = "typing-extensions" }, 146 | ] 147 | sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } 148 | wheels = [ 149 | { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, 150 | { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, 151 | { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, 152 | { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, 153 | { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, 154 | { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, 155 | { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, 156 | { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, 157 | { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, 158 | { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, 159 | { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, 160 | { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, 161 | { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, 162 | { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, 163 | { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, 164 | { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, 165 | { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, 166 | { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, 167 | { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, 168 | { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, 169 | { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, 170 | { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, 171 | { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, 172 | { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, 173 | { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, 174 | ] 175 | 176 | [[package]] 177 | name = "mypy-extensions" 178 | version = "1.1.0" 179 | source = { registry = "https://pypi.org/simple" } 180 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 181 | wheels = [ 182 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 183 | ] 184 | 185 | [[package]] 186 | name = "packaging" 187 | version = "25.0" 188 | source = { registry = "https://pypi.org/simple" } 189 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 190 | wheels = [ 191 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 192 | ] 193 | 194 | [[package]] 195 | name = "pathspec" 196 | version = "0.12.1" 197 | source = { registry = "https://pypi.org/simple" } 198 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 199 | wheels = [ 200 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 201 | ] 202 | 203 | [[package]] 204 | name = "pluggy" 205 | version = "1.6.0" 206 | source = { registry = "https://pypi.org/simple" } 207 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 208 | wheels = [ 209 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 210 | ] 211 | 212 | [[package]] 213 | name = "pyproject-hooks" 214 | version = "1.2.0" 215 | source = { registry = "https://pypi.org/simple" } 216 | sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } 217 | wheels = [ 218 | { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, 219 | ] 220 | 221 | [[package]] 222 | name = "pytest" 223 | version = "8.3.5" 224 | source = { registry = "https://pypi.org/simple" } 225 | dependencies = [ 226 | { name = "colorama", marker = "sys_platform == 'win32'" }, 227 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 228 | { name = "iniconfig" }, 229 | { name = "packaging" }, 230 | { name = "pluggy" }, 231 | { name = "tomli", marker = "python_full_version < '3.11'" }, 232 | ] 233 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 234 | wheels = [ 235 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 236 | ] 237 | 238 | [[package]] 239 | name = "pytest-cov" 240 | version = "6.1.1" 241 | source = { registry = "https://pypi.org/simple" } 242 | dependencies = [ 243 | { name = "coverage", extra = ["toml"] }, 244 | { name = "pytest" }, 245 | ] 246 | sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } 247 | wheels = [ 248 | { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, 249 | ] 250 | 251 | [[package]] 252 | name = "ruff" 253 | version = "0.11.12" 254 | source = { registry = "https://pypi.org/simple" } 255 | sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } 256 | wheels = [ 257 | { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, 258 | { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, 259 | { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, 260 | { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, 261 | { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, 262 | { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, 263 | { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, 264 | { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, 265 | { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, 266 | { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, 267 | { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, 268 | { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, 269 | { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, 270 | { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, 271 | { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, 272 | { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, 273 | { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, 274 | ] 275 | 276 | [[package]] 277 | name = "structlog" 278 | version = "25.3.0" 279 | source = { registry = "https://pypi.org/simple" } 280 | dependencies = [ 281 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 282 | ] 283 | sdist = { url = "https://files.pythonhosted.org/packages/ff/6a/b0b6d440e429d2267076c4819300d9929563b1da959cf1f68afbcd69fe45/structlog-25.3.0.tar.gz", hash = "sha256:8dab497e6f6ca962abad0c283c46744185e0c9ba900db52a423cb6db99f7abeb", size = 1367514, upload-time = "2025-04-25T16:00:39.167Z" } 284 | wheels = [ 285 | { url = "https://files.pythonhosted.org/packages/f5/52/7a2c7a317b254af857464da3d60a0d3730c44f912f8c510c76a738a207fd/structlog-25.3.0-py3-none-any.whl", hash = "sha256:a341f5524004c158498c3127eecded091eb67d3a611e7a3093deca30db06e172", size = 68240, upload-time = "2025-04-25T16:00:37.295Z" }, 286 | ] 287 | 288 | [[package]] 289 | name = "structlog-gcp" 290 | source = { editable = "." } 291 | dependencies = [ 292 | { name = "structlog" }, 293 | ] 294 | 295 | [package.dev-dependencies] 296 | dev = [ 297 | { name = "build", extra = ["uv"] }, 298 | { name = "mypy" }, 299 | { name = "pytest" }, 300 | { name = "pytest-cov" }, 301 | { name = "ruff" }, 302 | ] 303 | 304 | [package.metadata] 305 | requires-dist = [{ name = "structlog" }] 306 | 307 | [package.metadata.requires-dev] 308 | dev = [ 309 | { name = "build", extras = ["uv"], specifier = ">=1.2.2" }, 310 | { name = "mypy", specifier = ">=1.11.2" }, 311 | { name = "pytest", specifier = ">=8.3.3" }, 312 | { name = "pytest-cov", specifier = ">=5.0.0" }, 313 | { name = "ruff", specifier = ">=0.6.6" }, 314 | ] 315 | 316 | [[package]] 317 | name = "tomli" 318 | version = "2.2.1" 319 | source = { registry = "https://pypi.org/simple" } 320 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 321 | wheels = [ 322 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 323 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 324 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 325 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 326 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 327 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 328 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 329 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 330 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 331 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 332 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 333 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 334 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 335 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 336 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 337 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 338 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 339 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 340 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 341 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 342 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 343 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 344 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 345 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 346 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 347 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 348 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 349 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 350 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 351 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 352 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 353 | ] 354 | 355 | [[package]] 356 | name = "typing-extensions" 357 | version = "4.13.2" 358 | source = { registry = "https://pypi.org/simple" } 359 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 360 | wheels = [ 361 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 362 | ] 363 | 364 | [[package]] 365 | name = "uv" 366 | version = "0.7.9" 367 | source = { registry = "https://pypi.org/simple" } 368 | sdist = { url = "https://files.pythonhosted.org/packages/9a/7c/8621d5928111f985196dc75c50a64147b3bad39f36164686f24d45581367/uv-0.7.9.tar.gz", hash = "sha256:baac54e49f3b0d05ee83f534fdcb27b91d2923c585bf349a1532ca25d62c216f", size = 3272882, upload-time = "2025-05-30T19:54:33.003Z" } 369 | wheels = [ 370 | { url = "https://files.pythonhosted.org/packages/c5/7a/e4d12029e16f30279ef48f387545f8f3974dc3c4c9d8ef59c381ae7e6a7d/uv-0.7.9-py3-none-linux_armv6l.whl", hash = "sha256:0f8c53d411f95cec2fa19471c23b41ec456fc0d5f2efca96480d94e0c34026c2", size = 16746809, upload-time = "2025-05-30T19:53:35.447Z" }, 371 | { url = "https://files.pythonhosted.org/packages/fc/85/8df3ca683e1a260117efa31373e91e1c03a4862b7add865662f60a967fdf/uv-0.7.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:85c1a63669e49b825923fc876b7467cc3c20d4aa010f522c0ac8b0f30ce2b18e", size = 16821006, upload-time = "2025-05-30T19:53:40.102Z" }, 372 | { url = "https://files.pythonhosted.org/packages/77/d4/c40502ec8f5575798b7ec13ac38c0d5ded84cc32129c1d74a47f8cb7bc0a/uv-0.7.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa10c61668f94515acf93f31dbb8de41b1f2e7a9c41db828f2448cef786498ff", size = 15600148, upload-time = "2025-05-30T19:53:43.513Z" }, 373 | { url = "https://files.pythonhosted.org/packages/4f/dd/4deec6d5b556f4033d6bcc35d6aad70c08acea3f5da749cb34112dced5da/uv-0.7.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9de67ca9ea97db71e5697c1320508e25679fb68d4ee2cea27bbeac499a6bad56", size = 16038119, upload-time = "2025-05-30T19:53:46.504Z" }, 374 | { url = "https://files.pythonhosted.org/packages/cb/c5/2c23763e18566a9a7767738714791203cc97a7530979f61e0fd32d8473a2/uv-0.7.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13ce63524f88228152edf8a9c1c07cecc07d69a2853b32ecc02ac73538aaa5c1", size = 16467257, upload-time = "2025-05-30T19:53:49.592Z" }, 375 | { url = "https://files.pythonhosted.org/packages/da/94/f452d0093f466f9f81a2ede3ea2d48632237b79eb1dc595c7c91be309de5/uv-0.7.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3453b7bb65eaea87c9129e27bff701007a8bd1a563249982a1ede7ec4357ced6", size = 17170719, upload-time = "2025-05-30T19:53:52.828Z" }, 376 | { url = "https://files.pythonhosted.org/packages/69/bf/e15ef77520e9bbf00d29a3b639dfaf4fe63996863d6db00c53eba19535c7/uv-0.7.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d7b1e36a8b39600d0f0333bf50c966e83beeaaee1a38380ccb0f16ab45f351c3", size = 18052903, upload-time = "2025-05-30T19:53:56.237Z" }, 377 | { url = "https://files.pythonhosted.org/packages/32/9f/ebf3f9910121ef037c0fe9e7e7fb5f1c25b77d41a65a029d5cbcd85cc886/uv-0.7.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab412ed3d415f07192805788669c8a89755086cdd6fe9f021e1ba21781728031", size = 17771828, upload-time = "2025-05-30T19:53:59.561Z" }, 378 | { url = "https://files.pythonhosted.org/packages/fe/6c/82b4cd471432e721c239ddde2ebee2e674238f3bd88e279e6c71f3cbc775/uv-0.7.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbeb229ee86f69913f5f9236ff1b8ccbae212f559d7f029f8432fa8d9abcc7e0", size = 17886161, upload-time = "2025-05-30T19:54:02.865Z" }, 379 | { url = "https://files.pythonhosted.org/packages/63/e2/922d2eed25647b50a7257a7bfea10c36d9ff910d1451f9a1ba5e31766f41/uv-0.7.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d654a14d632ecb078969ae7252d89dd98c89205df567a1eff18b5f078a6d00", size = 17442630, upload-time = "2025-05-30T19:54:06.519Z" }, 380 | { url = "https://files.pythonhosted.org/packages/96/b8/45a5598cc8d466bb1669ccf0fc4f556719babfdb7a1983edc24967cb3845/uv-0.7.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f5f47e93a5f948f431ca55d765af6e818c925500807539b976bfda7f94369aa9", size = 16299207, upload-time = "2025-05-30T19:54:09.713Z" }, 381 | { url = "https://files.pythonhosted.org/packages/14/35/7e70639cd175f340138c88290c819214a496dfc52461f30f71e51e776293/uv-0.7.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:267fe25ad3adf024e13617be9fc99bedebf96bf726c6140e48d856e844f21af4", size = 16427594, upload-time = "2025-05-30T19:54:13.318Z" }, 382 | { url = "https://files.pythonhosted.org/packages/5e/f6/90fe538370bc60509cca942b703bca06c06c160ec09816ea6946882278d1/uv-0.7.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:473d3c6ee07588cff8319079d9225fb393ed177d8d57186fce0d7c1aebff79c0", size = 16751451, upload-time = "2025-05-30T19:54:16.833Z" }, 383 | { url = "https://files.pythonhosted.org/packages/09/cb/c099aba21fb22e50713b42e874075a5b60c6b4d141cc3868ae22f505baa7/uv-0.7.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:19792c88058894c10f0370a5e5492bb4a7e6302c439fed9882e73ba2e4b231ef", size = 17594496, upload-time = "2025-05-30T19:54:20.383Z" }, 384 | { url = "https://files.pythonhosted.org/packages/c0/2e/e35b2c5669533075987e1d74da45af891890ae5faee031f90997ed81cada/uv-0.7.9-py3-none-win32.whl", hash = "sha256:298e9b3c65742edcb3097c2cf3f62ec847df174a7c62c85fe139dddaa1b9ab65", size = 17121149, upload-time = "2025-05-30T19:54:23.608Z" }, 385 | { url = "https://files.pythonhosted.org/packages/33/8e/d10425711156d0d5d9a28299950acb3ab4a3987b3150a3c871ac95ce2fdd/uv-0.7.9-py3-none-win_amd64.whl", hash = "sha256:82d76ea988ff1347158c6de46a571b1db7d344219e452bd7b3339c21ec37cfd8", size = 18622895, upload-time = "2025-05-30T19:54:27.427Z" }, 386 | { url = "https://files.pythonhosted.org/packages/72/77/cac29a8fb608b5613b7a0863ec6bd7c2517f3a80b94c419e9d890c12257e/uv-0.7.9-py3-none-win_arm64.whl", hash = "sha256:4d419bcc3138fd787ce77305f1a09e2a984766e0804c6e5a2b54adfa55d2439a", size = 17316542, upload-time = "2025-05-30T19:54:30.697Z" }, 387 | ] 388 | 389 | [[package]] 390 | name = "zipp" 391 | version = "3.22.0" 392 | source = { registry = "https://pypi.org/simple" } 393 | sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257, upload-time = "2025-05-26T14:46:32.217Z" } 394 | wheels = [ 395 | { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796, upload-time = "2025-05-26T14:46:30.775Z" }, 396 | ] 397 | --------------------------------------------------------------------------------