diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb7de62..86f0f83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,23 +10,16 @@ on: tags: - "v*" -env: - PYTHON_VERSION: "3.8" - POETRY_VERSION: "1.5" - jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: chartboost/ruff-action@v1 + - uses: chartboost/ruff-action@v1 with: - python-version: ${{ env.PYTHON_VERSION }} - - run: pip install poetry==${{ env.POETRY_VERSION }} - - run: poetry install - - run: poetry run black --check axiom tests examples - - run: poetry run pylint -E axiom tests examples + args: 'format --check' test-integration: name: Test Integration @@ -36,33 +29,27 @@ jobs: needs: lint strategy: fail-fast: true - max-parallel: 1 matrix: - python: ["3.8", "3.9", "3.10", "3.11"] - environment: - - development - - staging - include: - - environment: development - url: TESTING_DEV_API_URL - token: TESTING_DEV_TOKEN - org_id: TESTING_DEV_ORG_ID - - environment: staging - url: TESTING_STAGING_API_URL - token: TESTING_STAGING_TOKEN - org_id: TESTING_STAGING_ORG_ID + python: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - run: pip install poetry==${{ env.POETRY_VERSION }} - - run: poetry install - - run: poetry run python -m pytest + - name: Install uv + uses: astral-sh/setup-uv@v3 + - name: Test against development + run: uv run pytest env: - AXIOM_URL: ${{ secrets[matrix.url] }} - AXIOM_TOKEN: ${{ secrets[matrix.token] }} - AXIOM_ORG_ID: ${{ secrets[matrix.org_id] }} + AXIOM_URL: ${{ secrets.TESTING_DEV_API_URL }} + AXIOM_TOKEN: ${{ secrets.TESTING_DEV_TOKEN }} + AXIOM_ORG_ID: ${{ secrets.TESTING_DEV_ORG_ID }} + - name: Test against staging + run: uv run pytest + env: + AXIOM_URL: ${{ secrets.TESTING_STAGING_API_URL }} + AXIOM_TOKEN: ${{ secrets.TESTING_STAGING_TOKEN }} + AXIOM_ORG_ID: ${{ secrets.TESTING_STAGING_ORG_ID }} publish: name: Publish on PyPi @@ -72,8 +59,13 @@ jobs: - test-integration steps: - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v3 - uses: actions/setup-python@v5 with: - python-version: ${{ env.PYTHON_VERSION }} - - run: pip install poetry==${{ env.POETRY_VERSION }} - - run: poetry publish --build -u __token__ -p "${{ secrets.PYPI_TOKEN }}" \ No newline at end of file + python-version-file: "pyproject.toml" + - run: uv build + - run: uvx twine upload dist/* + env: + TWINE_USERNAME: "__token__" + TWINE_PASSWORD: "${{ secrets.PYPI_TOKEN }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..806a063 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: local + hooks: + - id: ruff-check + name: ruff check --fix + entry: uv run ruff check --fix + language: system + types: [python] + - id: ruff-format + name: ruff format + entry: uv run ruff format + language: system + types: [python] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 5d547c4..0000000 --- a/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MASTER] -extension-pkg-whitelist=ujson diff --git a/README.md b/README.md index 14446e0..31bc891 100644 --- a/README.md +++ b/README.md @@ -49,41 +49,80 @@ Otherwise create a personal token in [the Axiom settings](https://cloud.axiom.co You can also configure the client using options passed to the client constructor: ```py -import axiom +import axiom_py -client = axiom.Client("", "") +client = axiom_py.Client("", "") ``` Create and use a client like this: ```py -import axiom +import axiom_py import rfc3339 from datetime import datetime,timedelta -client = axiom.Client() - -time = datetime.utcnow() - timedelta(hours=1) -time_formatted = rfc3339.format(time) +client = axiom_py.Client() client.ingest_events( dataset="my-dataset", events=[ - {"foo": "bar", "_time": time_formatted}, - {"bar": "baz", "_time": time_formatted}, + {"foo": "bar"}, + {"bar": "baz"}, ]) client.query(r"['my-dataset'] | where foo == 'bar' | limit 100") ``` -for more examples, check out the [examples](examples) directory. +For more examples, see [`examples/client.py`](examples/client.py). + +## Logger + +You can use the `AxiomHandler` to send logs from the `logging` module to Axiom +like this: + +```python +import axiom_py +from axiom_py.logging import AxiomHandler +import logging + + +def setup_logger(): + client = axiom_py.Client() + handler = AxiomHandler(client, "my-dataset") + logging.getLogger().addHandler(handler) +``` + +For a full example, see [`examples/logger.py`](examples/logger.py). + +If you use [structlog](https://github.com/hynek/structlog), you can set up the +`AxiomProcessor` like this: + +```python +from axiom_py import Client +from axiom_py.structlog import AxiomProcessor + + +def setup_logger(): + client = Client() + + structlog.configure( + processors=[ + # ... + structlog.processors.add_log_level, + structlog.processors.TimeStamper(fmt="iso", key="_time"), + AxiomProcessor(client, "my-dataset"), + # ... + ] + ) +``` + +For a full example, see [`examples/structlog.py`](examples/structlog.py). ## Contributing -This project uses [Poetry](https://python-poetry.org) for dependecy management -and packaging, so make sure that this is installed (see [Poetry Installation](https://python-poetry.org/docs/#installation)). +This project uses [uv](https://docs.astral.sh/uv) for dependency management +and packaging, so make sure that this is installed. -Run `poetry install` to install dependencies and `poetry shell` to activate a -virtual environment. +To lint and format files before commit, run `uvx pre-commit install`. ## License diff --git a/axiom/__init__.py b/axiom/__init__.py deleted file mode 100644 index 8c42db4..0000000 --- a/axiom/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Axiom Python Client -""" - -__version__ = "0.1.0-beta.2" - -from .client import * -from .datasets import * -from .annotations import * -from .tokens import * diff --git a/axiom/datasets.py b/axiom/datasets.py deleted file mode 100644 index 61ba2e5..0000000 --- a/axiom/datasets.py +++ /dev/null @@ -1,140 +0,0 @@ -"""This package provides dataset models and methods as well as a DatasetClient""" - -import ujson -from logging import Logger -from requests import Session -from typing import List, Dict -from dataclasses import dataclass, asdict, field -from datetime import datetime, timedelta -from .util import Util - - -@dataclass -class Dataset: - """Represents an Axiom dataset""" - - id: str = field(init=False) - name: str - description: str - who: str - created: str - - -@dataclass -class DatasetCreateRequest: - """Request used to create a dataset""" - - name: str - description: str - - -@dataclass -class DatasetUpdateRequest: - """Request used to update a dataset""" - - description: str - - -@dataclass -class TrimRequest: - """MaxDuration marks the oldest timestamp an event can have before getting deleted.""" - - maxDuration: str - - -@dataclass -class Field: - """A field of a dataset""" - - name: str - description: str - type: str - unit: str - hidden: bool - - -@dataclass -class DatasetInfo: - """Information and statistics stored inside a dataset""" - - name: str - numBlocks: int - numEvents: int - numFields: int - inputBytes: int - inputBytesHuman: str - compressedBytes: int - compressedBytesHuman: str - minTime: datetime - maxTime: datetime - fields: List[Field] - who: str - created: datetime - - -class DatasetsClient: # pylint: disable=R0903 - """DatasetsClient has methods to manipulate datasets.""" - - session: Session - - def __init__(self, session: Session, logger: Logger): - self.session = session - self.logger = logger - - def get(self, id: str) -> Dataset: - """Get a dataset by id.""" - path = "/v1/datasets/%s" % id - res = self.session.get(path) - decoded_response = res.json() - return Util.from_dict(Dataset, decoded_response) - - def create(self, req: DatasetCreateRequest) -> Dataset: - """Create a dataset with the given properties.""" - path = "/v1/datasets" - res = self.session.post(path, data=ujson.dumps(asdict(req))) - ds = Util.from_dict(Dataset, res.json()) - self.logger.info(f"created new dataset: {ds.name}") - return ds - - def get_list(self) -> List[Dataset]: - """List all available datasets.""" - path = "/v1/datasets" - res = self.session.get(path) - - datasets = [] - for record in res.json(): - ds = Util.from_dict(Dataset, record) - datasets.append(ds) - - return datasets - - def update(self, id: str, req: DatasetUpdateRequest) -> Dataset: - """Update a dataset with the given properties.""" - path = "/v1/datasets/%s" % id - res = self.session.put(path, data=ujson.dumps(asdict(req))) - ds = Util.from_dict(Dataset, res.json()) - self.logger.info(f"updated dataset({ds.name}) with new desc: {ds.description}") - return ds - - def delete(self, id: str): - """Deletes a dataset with the given id.""" - path = "/v1/datasets/%s" % id - self.session.delete(path) - - def trim(self, id: str, maxDuration: timedelta): - """ - Trim the dataset identified by its id to a given length. The max duration - given will mark the oldest timestamp an event can have. Older ones will be - deleted from the dataset. - """ - path = "/v1/datasets/%s/trim" % id - # prepare request payload and format masDuration to append time unit at the end, e.g `1s` - req = TrimRequest(f"{maxDuration.seconds}s") - self.session.post(path, data=ujson.dumps(asdict(req))) - - def info(self, id: str) -> DatasetInfo: - """Returns the info about a dataset.""" - path = "/v1/datasets/%s/info" % id - res = self.session.get(path) - decoded_response = res.json() - return Util.from_dict(DatasetInfo, decoded_response) diff --git a/axiom/query/__init__.py b/axiom/query/__init__.py deleted file mode 100644 index 78e2826..0000000 --- a/axiom/query/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .query import * -from .options import * -from .filter import * -from .aggregation import * -from .result import * diff --git a/axiom/users.py b/axiom/users.py deleted file mode 100644 index 1c5d968..0000000 --- a/axiom/users.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import List -from .util import Util -from dataclasses import dataclass -from requests import Session - - -@dataclass -class User: - """An authenticated axiom user.""" - - id: str - name: str - emails: List[str] - - -class UsersClient: - """The UsersClient is a client for the Axiom Users service.""" - - def __init__(self, session: Session): - self.session = session - - def current(self) -> User: - """Get the current authenticated user.""" - res = self.session.get("/v1/user") - user = Util.from_dict(User, res.json()) - return user diff --git a/axiom/util.py b/axiom/util.py deleted file mode 100644 index 3874f2b..0000000 --- a/axiom/util.py +++ /dev/null @@ -1,76 +0,0 @@ -import dacite -import iso8601 -from enum import Enum -from uuid import UUID -from typing import Type, TypeVar -from datetime import datetime, timedelta - -from .query import QueryKind -from .query.aggregation import AggregationOperation -from .query.result import MessageCode, MessagePriority -from .query.filter import FilterOperation - - -T = TypeVar("T") - - -class Util: - """A collection of helper methods.""" - - @classmethod - def from_dict(cls, data_class: Type[T], data) -> T: - cfg = dacite.Config( - type_hooks={ - QueryKind: QueryKind, - datetime: cls.convert_string_to_datetime, - AggregationOperation: AggregationOperation, - FilterOperation: FilterOperation, - MessageCode: MessageCode, - MessagePriority: MessagePriority, - timedelta: cls.convert_string_to_timedelta, - } - ) - - return dacite.from_dict(data_class=data_class, data=data, config=cfg) - - @classmethod - def convert_string_to_datetime(cls, val: str) -> datetime: - d = iso8601.parse_date(val) - return d - - @classmethod - def convert_string_to_timedelta(cls, val: str) -> timedelta: - if val == "0": - return timedelta(seconds=0) - - exp = "^([0-9]?)([a-z])$" - import re - - found = re.search(exp, val) - if not found: - raise Exception(f"failed to parse timedelta field from value {val}") - - v = int(found.groups()[0]) - unit = found.groups()[1] - - if unit == "s": - return timedelta(seconds=v) - elif unit == "m": - return timedelta(minutes=v) - elif unit == "h": - return timedelta(hours=v) - elif unit == "d": - return timedelta(days=v) - else: - raise Exception(f"failed to parse timedelta field from value {val}") - - @classmethod - def handle_json_serialization(cls, obj): - if isinstance(obj, datetime): - return obj.isoformat("T") + "Z" - elif isinstance(obj, timedelta): - return str(obj.seconds) + "s" - elif isinstance(obj, Enum): - return obj.value - elif isinstance(obj, UUID): - return str(obj) diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index ec14f59..0000000 --- a/examples/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Examples - -## Usage - -```shell -export AXIOM_TOKEN="..." -python -``` - - -## Examples - -- [ingest.py](ingest.py) - Ingest events into Axiom -- [query.py](query.py) - Query a dataset as part of the request -- [query_legacy.py](query_legacy.py) - Query a dataset using the legacy query method -- [create_dateset.py](create_dataset.py) - Create a new dataset -- [list_datasets.py](list_datasets.py) - Retrieve a list of all datasets -- [delete_dataset.py](delete_dataset.py) - Delete a dataset -- diff --git a/examples/client_example.py b/examples/client_example.py new file mode 100644 index 0000000..a09f779 --- /dev/null +++ b/examples/client_example.py @@ -0,0 +1,32 @@ +from axiom_py import Client + + +def main(): + client = Client() + dataset_name = "my-dataset" + + # Get current user + print(client.users.current()) + + # List datasets + res = client.datasets.get_list() + for dataset in res: + print(dataset.name) + + # Create a dataset + client.datasets.create(dataset_name, "A description.") + + # Ingest events + client.ingest_events(dataset_name, [{"foo": "bar"}]) + + # Query events + res = client.query(f"['{dataset_name}'] | where status == 500") + for match in res.matches: + print(match.data) + + # Delete the dataset + client.datasets.delete(dataset_name) + + +if __name__ == "__main__": + main() diff --git a/examples/create_dataset.py b/examples/create_dataset.py deleted file mode 100644 index d5de57b..0000000 --- a/examples/create_dataset.py +++ /dev/null @@ -1,9 +0,0 @@ -from axiom import Client, DatasetCreateRequest - - -def create_dataset(dataset_name): - client = Client() - res = client.datasets.create( - DatasetCreateRequest(name=dataset_name, description="") - ) - print(f"created dataset: {res.id}") diff --git a/examples/delete_dataset.py b/examples/delete_dataset.py deleted file mode 100644 index ab5d6e9..0000000 --- a/examples/delete_dataset.py +++ /dev/null @@ -1,7 +0,0 @@ -from axiom import Client, DatasetCreateRequest - - -def delete_dataset(dataset_name): - client = Client() - client.datasets.delete(dataset_name) - print(f"deleted dataset: my-dateset") diff --git a/examples/ingest.py b/examples/ingest.py deleted file mode 100644 index 8d133ab..0000000 --- a/examples/ingest.py +++ /dev/null @@ -1,7 +0,0 @@ -from axiom import Client - - -def ingest(dataset_name): - client = Client() - res = client.ingest_events(dataset_name, [{"foo": "bar"}]) - print("Ingested %d events with %d failures".format(res.ingested, res.failed)) diff --git a/examples/list_datasets.py b/examples/list_datasets.py deleted file mode 100644 index 47fdc8d..0000000 --- a/examples/list_datasets.py +++ /dev/null @@ -1,8 +0,0 @@ -from axiom import Client - - -def list_datasets(): - client = Client() - res = client.datasets.get_list() - for dataset in res: - print(f"found dataset: {dataset.name}") diff --git a/examples/logger_example.py b/examples/logger_example.py new file mode 100644 index 0000000..5bf1edf --- /dev/null +++ b/examples/logger_example.py @@ -0,0 +1,19 @@ +import axiom_py +from axiom_py.logging import AxiomHandler +import logging + + +def main(): + # Add Axiom handler to root logger + client = axiom_py.Client() + handler = AxiomHandler(client, "my-dataset") + logging.getLogger().addHandler(handler) + + # Get logger and log something + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + logger.info("Hello world") + + +if __name__ == "__main__": + main() diff --git a/examples/main.py b/examples/main.py deleted file mode 100644 index 6e551a4..0000000 --- a/examples/main.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -from create_dataset import create_dataset -from delete_dataset import delete_dataset -from list_datasets import list_datasets -from ingest import ingest -from query import query -from query_legacy import queryLegacy - - -def main(): - dataset_name = "my-dataset" - # create a new dataset - create_dataset(dataset_name) - list_datasets() - # ingest some data - ingest(dataset_name) - # query the ingested data - query(dataset_name) - queryLegacy(dataset_name) - # finally, delete the dataset - delete_dataset(dataset_name) - - -if __name__ == "__main__": - main() diff --git a/examples/pyproject.toml b/examples/pyproject.toml deleted file mode 100644 index 989e5cf..0000000 --- a/examples/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[tool.poetry] -name = "axiom-py-examples" -version = "0.1" -description = "Axiom API Python bindings examples." -authors = ["Axiom, Inc."] -license = "MIT" - -[tool.poetry.dependencies] -python = "^3.8" -axiom-py = { path = "../" } - - -[tool.poetry.dev-dependencies] -pylint = "^2.7.2" - - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" diff --git a/examples/query.py b/examples/query.py deleted file mode 100644 index a266a8d..0000000 --- a/examples/query.py +++ /dev/null @@ -1,10 +0,0 @@ -from axiom import Client - - -def query(dataset_name): - aplQuery = f"['{dataset_name}'] | where status == 500" - - client = Client() - res = client.query(aplQuery) - for match in res.matches: - print(match.data) diff --git a/examples/query_legacy.py b/examples/query_legacy.py deleted file mode 100644 index f89c721..0000000 --- a/examples/query_legacy.py +++ /dev/null @@ -1,19 +0,0 @@ -from axiom import Client, QueryLegacy, QueryOptions, QueryKind -from datetime import datetime, timedelta - - -def queryLegacy(dataset_name): - endTime = datetime.now() - startTime = endTime - timedelta(days=1) - query = QueryLegacy(startTime=startTime, endTime=endTime) - - client = Client() - res = client.query_legacy( - dataset_name, query, QueryOptions(saveAsKind=QueryKind.ANALYTICS) - ) - if res.matches is None or len(res.matches) == 0: - print("No matches found") - return - - for match in res.matches: - print(match.data) diff --git a/examples/structlog_example.py b/examples/structlog_example.py new file mode 100644 index 0000000..21585ab --- /dev/null +++ b/examples/structlog_example.py @@ -0,0 +1,26 @@ +from axiom_py import Client +from axiom_py.structlog import AxiomProcessor +import structlog + + +def main(): + client = Client() + + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.processors.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.set_exc_info, + structlog.processors.TimeStamper(fmt="iso", key="_time"), + AxiomProcessor(client, "my-dataset"), + structlog.dev.ConsoleRenderer(), + ] + ) + + log = structlog.get_logger() + log.info("hello", who="world") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index e1000fe..45d6dcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,32 +1,50 @@ -[tool.poetry] +[project] name = "axiom-py" -version = "0.5.0" -description = "Axiom API Python bindings." -authors = ["Axiom, Inc."] -license = "MIT" -packages = [ - { include = "axiom" }, +version = "0.8.0" +description = "Official bindings for the Axiom API" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "iso8601>=1.0.2", + "requests>=2.32.3", + "requests-toolbelt>=1.0.0", + "ujson>=5.10.0", + "dacite>=1.8.1", + "pyhumps>=3.8.0", + "ndjson>=0.3.1", ] +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Topic :: System :: Logging", -[tool.poetry.dependencies] -# urllib 3 has a breaking change in 2.0.0 -urllib3 = "<3.0.0" -python = "^3.8" -requests = "^2.30.0" -requests-toolbelt = ">= 0.9.1, <1.1.0" -ujson = "^5.2.0" -dacite = "^1.6.0" -pyhumps = ">=1.6.1,<4.0.0" -ndjson = "^0.3.1" -rfc3339 = "^6.2" -iso8601 = ">=1.0.2,<3.0.0" + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] -[tool.poetry.dev-dependencies] -black = "^24.4.2" -pytest = "^8.2.1" -pylint = "^3.2.1" -responses = "^0.25.0" +[project.urls] +Homepage = "https://axiom.co" +Repository = "https://github.com/axiomhq/axiom-py.git" +Issues = "https://github.com/axiomhq/axiom-py/issues" [build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.ruff] +line-length = 79 # PEP 8 + +[tool.uv] +dev-dependencies = [ + "ruff>=0.6.4", + "pytest>=8.3.2", + "responses>=0.25.3", + "rfc3339>=6.2", + "iso8601>=1.0.2", + "pre-commit>=3.5.0", +] diff --git a/shell.nix b/shell.nix deleted file mode 100644 index d5ac036..0000000 --- a/shell.nix +++ /dev/null @@ -1,8 +0,0 @@ -with import {}; - -mkShell { - nativeBuildInputs = with buildPackages; [ - python38 - python38Packages.poetry - ]; -} diff --git a/src/axiom_py/__init__.py b/src/axiom_py/__init__.py new file mode 100644 index 0000000..07358a0 --- /dev/null +++ b/src/axiom_py/__init__.py @@ -0,0 +1,47 @@ +""" +Axiom Python Client +""" + +from .client import ( + AxiomError, + IngestFailure, + IngestStatus, + IngestOptions, + AplResultFormat, + ContentType, + ContentEncoding, + WrongQueryKindException, + AplOptions, + Client, +) +from .datasets import ( + Dataset, + TrimRequest, + DatasetsClient, +) +from .annotations import ( + Annotation, + AnnotationCreateRequest, + AnnotationUpdateRequest, + AnnotationsClient, +) + +_all_ = [ + AxiomError, + IngestFailure, + IngestStatus, + IngestOptions, + AplResultFormat, + ContentType, + ContentEncoding, + WrongQueryKindException, + AplOptions, + Client, + Dataset, + TrimRequest, + DatasetsClient, + Annotation, + AnnotationCreateRequest, + AnnotationUpdateRequest, + AnnotationsClient, +] diff --git a/axiom/annotations.py b/src/axiom_py/annotations.py similarity index 72% rename from axiom/annotations.py rename to src/axiom_py/annotations.py index 365d3a7..91e056c 100644 --- a/axiom/annotations.py +++ b/src/axiom_py/annotations.py @@ -3,11 +3,11 @@ import ujson from logging import Logger from requests import Session -from typing import List, Dict, Optional +from typing import List, Optional from dataclasses import dataclass, asdict, field -from datetime import datetime, timedelta +from datetime import datetime from urllib.parse import urlencode -from .util import Util +from .util import from_dict @dataclass @@ -60,17 +60,25 @@ def __init__(self, session: Session, logger: Logger): self.logger = logger def get(self, id: str) -> Annotation: - """Get a annotation by id.""" + """ + Get a annotation by id. + + See https://axiom.co/docs/restapi/endpoints/getAnnotation + """ path = "/v2/annotations/%s" % id res = self.session.get(path) decoded_response = res.json() - return Util.from_dict(Annotation, decoded_response) + return from_dict(Annotation, decoded_response) def create(self, req: AnnotationCreateRequest) -> Annotation: - """Create an annotation with the given properties.""" + """ + Create an annotation with the given properties. + + See https://axiom.co/docs/restapi/endpoints/createAnnotation + """ path = "/v2/annotations" res = self.session.post(path, data=ujson.dumps(asdict(req))) - annotation = Util.from_dict(Annotation, res.json()) + annotation = from_dict(Annotation, res.json()) self.logger.info(f"created new annotation: {annotation.id}") return annotation @@ -80,13 +88,17 @@ def list( start: Optional[datetime] = None, end: Optional[datetime] = None, ) -> List[Annotation]: - """List all annotations.""" + """ + List all annotations. + + See https://axiom.co/docs/restapi/endpoints/getAnnotations + """ query_params = {} if len(datasets) > 0: query_params["datasets"] = ",".join(datasets) - if start != None: + if start is not None: query_params["start"] = start.isoformat() - if end != None: + if end is not None: query_params["end"] = end.isoformat() path = f"/v2/annotations?{urlencode(query_params, doseq=True)}" @@ -94,20 +106,28 @@ def list( annotations = [] for record in res.json(): - ds = Util.from_dict(Annotation, record) + ds = from_dict(Annotation, record) annotations.append(ds) return annotations def update(self, id: str, req: AnnotationUpdateRequest) -> Annotation: - """Update an annotation with the given properties.""" + """ + Update an annotation with the given properties. + + See https://axiom.co/docs/restapi/endpoints/updateAnnotation + """ path = "/v2/annotations/%s" % id res = self.session.put(path, data=ujson.dumps(asdict(req))) - annotation = Util.from_dict(Annotation, res.json()) + annotation = from_dict(Annotation, res.json()) self.logger.info(f"updated annotation({annotation.id})") return annotation def delete(self, id: str): - """Deletes an annotation with the given id.""" + """ + Deletes an annotation with the given id. + + See https://axiom.co/docs/restapi/endpoints/deleteAnnotation + """ path = "/v2/annotations/%s" % id self.session.delete(path) diff --git a/axiom/client.py b/src/axiom_py/client.py similarity index 75% rename from axiom/client.py rename to src/axiom_py/client.py index 5897aea..4ad9068 100644 --- a/axiom/client.py +++ b/src/axiom_py/client.py @@ -1,13 +1,11 @@ """Client provides an easy-to use client library to connect to Axiom.""" import ndjson -import dacite +import atexit import gzip import ujson import os -from .tokens import TokenAttributes, Token -from .util import Util from enum import Enum from humps import decamelize from typing import Optional, List, Dict @@ -15,25 +13,25 @@ from dataclasses import asdict, dataclass, field from datetime import datetime from requests_toolbelt.sessions import BaseUrlSession -from requests_toolbelt.utils.dump import dump_response from requests.adapters import HTTPAdapter, Retry from .datasets import DatasetsClient -from .query import QueryLegacy, QueryResult, QueryOptions, QueryLegacyResult, QueryKind +from .query import ( + QueryLegacy, + QueryResult, + QueryOptions, + QueryLegacyResult, + QueryKind, +) from .annotations import AnnotationsClient from .users import UsersClient -from .__init__ import __version__ +from .version import __version__ +from .util import from_dict, handle_json_serialization, is_personal_token +from .tokens import TokenAttributes, Token AXIOM_URL = "https://api.axiom.co" -@dataclass -class Error: - status: Optional[int] = field(default=None) - message: Optional[str] = field(default=None) - error: Optional[str] = field(default=None) - - @dataclass class IngestFailure: """The ingestion failure of a single event""" @@ -75,6 +73,7 @@ class AplResultFormat(Enum): """The result format of an APL query.""" Legacy = "legacy" + Tabular = "tabular" class ContentType(Enum): @@ -114,25 +113,35 @@ class AplOptions: includeCursor: bool = field(default=False) -def raise_response_error(r): - if r.status_code >= 400: - print("==== Response Debugging ====") - print("##Request Headers", r.request.headers) +class AxiomError(Exception): + """This exception is raised on request errors.""" + + status: int + message: str - # extract content type - ct = r.headers["content-type"].split(";")[0] - if ct == ContentType.JSON.value: - dump = dump_response(r) - print(dump) - print("##Response:", dump.decode("UTF-8")) - err = dacite.from_dict(data_class=Error, data=r.json()) - print(err) - elif ct == ContentType.NDJSON.value: - decoded = ndjson.loads(r.text) - print("##Response:", decoded) + @dataclass + class Response: + message: str + error: Optional[str] - r.raise_for_status() - # TODO: Decode JSON https://github.com/axiomhq/axiom-go/blob/610cfbd235d3df17f96a4bb156c50385cfbd9edd/axiom/error.go#L35-L50 + def __init__(self, status: int, res: Response): + message = res.error if res.error is not None else res.message + super().__init__(f"API error {status}: {message}") + + self.status = status + self.message = message + + +def raise_response_error(res): + if res.status_code >= 400: + try: + error_res = from_dict(AxiomError.Response, res.json()) + except Exception: + # Response is not in the Axiom JSON format, create generic error + # message + error_res = AxiomError.Response(message=res.reason, error=None) + + raise AxiomError(res.status_code, error_res) class Client: # pylint: disable=R0903 @@ -141,6 +150,7 @@ class Client: # pylint: disable=R0903 datasets: DatasetsClient users: UsersClient annotations: AnnotationsClient + is_closed: bool # track if the client has been closed (for tests) def __init__( self, @@ -185,9 +195,17 @@ def __init__( self.session.headers.update({"X-Axiom-Org-Id": org_id}) self.datasets = DatasetsClient(self.session, self.logger) - self.users = UsersClient(self.session) + self.users = UsersClient(self.session, is_personal_token(token)) self.annotations = AnnotationsClient(self.session, self.logger) + # wrap shutdown hook in a lambda passing in self as a ref + atexit.register(lambda: self.shutdown_hook()) + self.is_closed = False + + def shutdown_hook(self): + self.session.close() + self.is_closed = True + def ingest( self, dataset: str, @@ -196,25 +214,27 @@ def ingest( enc: ContentEncoding, opts: Optional[IngestOptions] = None, ) -> IngestStatus: - """Ingest the events into the named dataset and returns the status.""" - path = "/v1/datasets/%s/ingest" % dataset - - # check if passed content type and encoding are correct - if not contentType: - raise ValueError("unknown content-type, choose one of json,x-ndjson or csv") + """ + Ingest the payload into the named dataset and returns the status. - if not enc: - raise ValueError("unknown content-encoding") + See https://axiom.co/docs/restapi/endpoints/ingestIntoDataset + """ + path = "/v1/datasets/%s/ingest" % dataset # set headers - headers = {"Content-Type": contentType.value, "Content-Encoding": enc.value} + headers = { + "Content-Type": contentType.value, + "Content-Encoding": enc.value, + } # prepare query params params = self._prepare_ingest_options(opts) # override the default header and set the value from the passed parameter - res = self.session.post(path, data=payload, headers=headers, params=params) + res = self.session.post( + path, data=payload, headers=headers, params=params + ) status_snake = decamelize(res.json()) - return Util.from_dict(IngestStatus, status_snake) + return from_dict(IngestStatus, status_snake) def ingest_events( self, @@ -222,11 +242,15 @@ def ingest_events( events: List[dict], opts: Optional[IngestOptions] = None, ) -> IngestStatus: - """Ingest the events into the named dataset and returns the status.""" + """ + Ingest the events into the named dataset and returns the status. + + See https://axiom.co/docs/restapi/endpoints/ingestIntoDataset + """ # encode request payload to NDJSON - content = ndjson.dumps(events, default=Util.handle_json_serialization).encode( - "UTF-8" - ) + content = ndjson.dumps( + events, default=handle_json_serialization + ).encode("UTF-8") gzipped = gzip.compress(content) return self.ingest( @@ -236,7 +260,11 @@ def ingest_events( def query_legacy( self, id: str, query: QueryLegacy, opts: QueryOptions ) -> QueryLegacyResult: - """Executes the given query on the dataset identified by its id.""" + """ + Executes the given structured query on the dataset identified by its id. + + See https://axiom.co/docs/restapi/endpoints/queryDataset + """ if not opts.saveAsKind or (opts.saveAsKind == QueryKind.APL): raise WrongQueryKindException( "invalid query kind %s: must be %s or %s" @@ -244,32 +272,44 @@ def query_legacy( ) path = "/v1/datasets/%s/query" % id - payload = ujson.dumps(asdict(query), default=Util.handle_json_serialization) + payload = ujson.dumps(asdict(query), default=handle_json_serialization) self.logger.debug("sending query %s" % payload) params = self._prepare_query_options(opts) res = self.session.post(path, data=payload, params=params) - result = Util.from_dict(QueryLegacyResult, res.json()) + result = from_dict(QueryLegacyResult, res.json()) self.logger.debug(f"query result: {result}") query_id = res.headers.get("X-Axiom-History-Query-Id") self.logger.info(f"received query result with query_id: {query_id}") result.savedQueryID = query_id return result - def apl_query(self, apl: str, opts: Optional[AplOptions] = None) -> QueryResult: - """Executes the given apl query on the dataset identified by its id.""" + def apl_query( + self, apl: str, opts: Optional[AplOptions] = None + ) -> QueryResult: + """ + Executes the given apl query on the dataset identified by its id. + + See https://axiom.co/docs/restapi/endpoints/queryApl + """ return self.query(apl, opts) - def query(self, apl: str, opts: Optional[AplOptions] = None) -> QueryResult: - """Executes the given apl query on the dataset identified by its id.""" + def query( + self, apl: str, opts: Optional[AplOptions] = None + ) -> QueryResult: + """ + Executes the given apl query on the dataset identified by its id. + + See https://axiom.co/docs/restapi/endpoints/queryApl + """ path = "/v1/datasets/_apl" payload = ujson.dumps( self._prepare_apl_payload(apl, opts), - default=Util.handle_json_serialization, + default=handle_json_serialization, ) self.logger.debug("sending query %s" % payload) params = self._prepare_apl_options(opts) res = self.session.post(path, data=payload, params=params) - result = Util.from_dict(QueryResult, res.json()) + result = from_dict(QueryResult, res.json()) self.logger.debug(f"apl query result: {result}") query_id = res.headers.get("X-Axiom-History-Query-Id") self.logger.info(f"received query result with query_id: {query_id}") @@ -281,7 +321,7 @@ def create_api_token(self, opts: TokenAttributes) -> Token: """Creates a new API token with permissions specified in a TokenAttributes object.""" res = self.session.post( "/v2/tokens", - data=ujson.dumps(asdict(opts), default=Util.handle_json_serialization), + data=ujson.dumps(asdict(opts), default=handle_json_serialization), ) # Return the new token and ID. @@ -328,7 +368,9 @@ def _prepare_ingest_options( return params - def _prepare_apl_options(self, opts: Optional[AplOptions]) -> Dict[str, object]: + def _prepare_apl_options( + self, opts: Optional[AplOptions] + ) -> Dict[str, object]: """Prepare the apl query options for the request.""" params = {"format": AplResultFormat.Legacy.value} diff --git a/src/axiom_py/datasets.py b/src/axiom_py/datasets.py new file mode 100644 index 0000000..c293b75 --- /dev/null +++ b/src/axiom_py/datasets.py @@ -0,0 +1,152 @@ +""" +This package provides dataset models and methods as well as a DatasetClient +""" + +import ujson +from logging import Logger +from requests import Session +from typing import List +from dataclasses import dataclass, asdict, field +from datetime import timedelta +from .util import from_dict + + +@dataclass +class Dataset: + """Represents an Axiom dataset""" + + id: str = field(init=False) + name: str + description: str + who: str + created: str + + +@dataclass +class DatasetCreateRequest: + """Request used to create a dataset""" + + name: str + description: str + + +@dataclass +class DatasetUpdateRequest: + """Request used to update a dataset""" + + description: str + + +@dataclass +class TrimRequest: + """ + MaxDuration marks the oldest timestamp an event can have before getting + deleted. + """ + + maxDuration: str + + +class DatasetsClient: # pylint: disable=R0903 + """DatasetsClient has methods to manipulate datasets.""" + + session: Session + + def __init__(self, session: Session, logger: Logger): + self.session = session + self.logger = logger + + def get(self, id: str) -> Dataset: + """ + Get a dataset by id. + + See https://axiom.co/docs/restapi/endpoints/getDataset + """ + path = "/v1/datasets/%s" % id + res = self.session.get(path) + decoded_response = res.json() + return from_dict(Dataset, decoded_response) + + def create(self, name: str, description: str = "") -> Dataset: + """ + Create a dataset with the given properties. + + See https://axiom.co/docs/restapi/endpoints/createDataset + """ + path = "/v1/datasets" + res = self.session.post( + path, + data=ujson.dumps( + asdict( + DatasetCreateRequest( + name=name, + description=description, + ) + ) + ), + ) + ds = from_dict(Dataset, res.json()) + self.logger.info(f"created new dataset: {ds.name}") + return ds + + def get_list(self) -> List[Dataset]: + """ + List all available datasets. + + See https://axiom.co/docs/restapi/endpoints/getDatasets + """ + path = "/v1/datasets" + res = self.session.get(path) + + datasets = [] + for record in res.json(): + ds = from_dict(Dataset, record) + datasets.append(ds) + + return datasets + + def update(self, id: str, new_description: str) -> Dataset: + """ + Update a dataset with the given properties. + + See https://axiom.co/docs/restapi/endpoints/updateDataset + """ + path = "/v1/datasets/%s" % id + res = self.session.put( + path, + data=ujson.dumps( + asdict( + DatasetUpdateRequest( + description=new_description, + ) + ) + ), + ) + ds = from_dict(Dataset, res.json()) + self.logger.info( + f"updated dataset({ds.name}) with new desc: {ds.description}" + ) + return ds + + def delete(self, id: str): + """ + Deletes a dataset with the given id. + + See https://axiom.co/docs/restapi/endpoints/deleteDataset + """ + path = "/v1/datasets/%s" % id + self.session.delete(path) + + def trim(self, id: str, maxDuration: timedelta): + """ + Trim the dataset identified by its id to a given length. The max + duration given will mark the oldest timestamp an event can have. + Older ones will be deleted from the dataset. + + See https://axiom.co/docs/restapi/endpoints/trimDataset + """ + path = "/v1/datasets/%s/trim" % id + # prepare request payload and format masDuration to append time unit at + # the end, e.g `1s` + req = TrimRequest(f"{maxDuration.seconds}s") + self.session.post(path, data=ujson.dumps(asdict(req))) diff --git a/axiom/logging.py b/src/axiom_py/logging.py similarity index 88% rename from axiom/logging.py rename to src/axiom_py/logging.py index 6916336..f6d0aac 100644 --- a/axiom/logging.py +++ b/src/axiom_py/logging.py @@ -12,12 +12,12 @@ class AxiomHandler(Handler): client: Client dataset: str - logcache: list + buffer: list interval: int last_run: float def __init__(self, client: Client, dataset: str, level=NOTSET, interval=1): - Handler.__init__(self, level) + super().__init__() # set urllib3 logging level to warning, check: # https://github.com/axiomhq/axiom-py/issues/23 # This is a temp solution that would stop requests @@ -35,7 +35,10 @@ def __init__(self, client: Client, dataset: str, level=NOTSET, interval=1): def emit(self, record): """emit sends a log to Axiom.""" self.buffer.append(record.__dict__) - if len(self.buffer) >= 1000 or time.monotonic() - self.last_run > self.interval: + if ( + len(self.buffer) >= 1000 + or time.monotonic() - self.last_run > self.interval + ): self.flush() def flush(self): diff --git a/src/axiom_py/query/__init__.py b/src/axiom_py/query/__init__.py new file mode 100644 index 0000000..d674673 --- /dev/null +++ b/src/axiom_py/query/__init__.py @@ -0,0 +1,40 @@ +from .query import QueryKind, Order, VirtualField, Projection, QueryLegacy +from .options import QueryOptions +from .filter import FilterOperation, BaseFilter, Filter +from .aggregation import AggregationOperation, Aggregation +from .result import ( + MessagePriority, + Message, + QueryStatus, + Entry, + EntryGroupAgg, + EntryGroup, + Interval, + Timeseries, + QueryLegacyResult, + QueryResult, +) + +__all__ = ( + QueryKind, + Order, + VirtualField, + Projection, + QueryLegacy, + QueryOptions, + FilterOperation, + BaseFilter, + Filter, + AggregationOperation, + Aggregation, + MessagePriority, + Message, + QueryStatus, + Entry, + EntryGroupAgg, + EntryGroup, + Interval, + Timeseries, + QueryLegacyResult, + QueryResult, +) diff --git a/axiom/query/aggregation.py b/src/axiom_py/query/aggregation.py similarity index 100% rename from axiom/query/aggregation.py rename to src/axiom_py/query/aggregation.py diff --git a/axiom/query/filter.py b/src/axiom_py/query/filter.py similarity index 100% rename from axiom/query/filter.py rename to src/axiom_py/query/filter.py diff --git a/axiom/query/options.py b/src/axiom_py/query/options.py similarity index 100% rename from axiom/query/options.py rename to src/axiom_py/query/options.py diff --git a/axiom/query/query.py b/src/axiom_py/query/query.py similarity index 93% rename from axiom/query/query.py rename to src/axiom_py/query/query.py index bbf257a..ceed063 100644 --- a/axiom/query/query.py +++ b/src/axiom_py/query/query.py @@ -1,7 +1,7 @@ from enum import Enum from typing import List, Optional from dataclasses import dataclass, field -from datetime import datetime, timedelta +from datetime import datetime from .aggregation import Aggregation from .filter import Filter @@ -62,7 +62,9 @@ class QueryLegacy: # serve-side auto-detection. resolution: str = field(default="auto") # Aggregations performed as part of the query. - aggregations: Optional[List[Aggregation]] = field(default_factory=lambda: []) + aggregations: Optional[List[Aggregation]] = field( + default_factory=lambda: [] + ) # GroupBy is a list of field names to group the query result by. Only valid # when at least one aggregation is specified. groupBy: Optional[List[str]] = field(default_factory=lambda: []) @@ -75,7 +77,9 @@ class QueryLegacy: limit: int = field(default=10) # VirtualFields is a list of virtual fields that can be referenced by # aggregations, filters and orders. - virtualFields: Optional[List[VirtualField]] = field(default_factory=lambda: []) + virtualFields: Optional[List[VirtualField]] = field( + default_factory=lambda: [] + ) # Projections is a list of projections that can be referenced by # aggregations, filters and orders. Leaving it empty projects all available # fields to the query result. diff --git a/axiom/query/result.py b/src/axiom_py/query/result.py similarity index 63% rename from axiom/query/result.py rename to src/axiom_py/query/result.py index 95a585c..ac6fd0c 100644 --- a/axiom/query/result.py +++ b/src/axiom_py/query/result.py @@ -1,21 +1,11 @@ from dataclasses import dataclass, field -from datetime import datetime, timedelta -from typing import List, Dict, Optional +from datetime import datetime +from typing import List, Dict, Optional, Union from enum import Enum from .query import QueryLegacy -class MessageCode(Enum): - """Message codes represents the code associated with the query.""" - - UNKNOWN_MESSAGE_CODE = "" - VIRTUAL_FIELD_FINALIZE_ERROR = "virtual_field_finalize_error" - MISSING_COLUMN = "missing_column" - LICENSE_LIMIT_FOR_QUERY_WARNING = "license_limit_for_query_warning" - DEFAULT_LIMIT_WARNING = "default_limit_warning" - - class MessagePriority(Enum): """Message priorities represents the priority of a message associated with a query.""" @@ -36,7 +26,7 @@ class Message: # describes how often a message of this type was raised by the query. count: int # code of the message. - code: MessageCode + code: str # a human readable text representation of the message. msg: str @@ -151,17 +141,122 @@ class QueryLegacyResult: savedQueryID: Optional[str] = field(default=None) +@dataclass +class Source: + name: str + + +@dataclass +class Order: + desc: bool + field: str + + +@dataclass +class Group: + name: str + + +@dataclass +class Range: + # Start is the starting time the query is limited by. + start: datetime + # End is the ending time the query is limited by. + end: datetime + # Field specifies the field name on which the query range was restricted. + field: str + + +@dataclass +class Aggregation: + # Args specifies any non-field arguments for the aggregation. + args: Optional[List[object]] + # Fields specifies the names of the fields this aggregation is computed on. + fields: Optional[List[str]] + # Name is the system name of the aggregation. + name: str + + +@dataclass +class Field: + name: str + type: str + agg: Optional[Aggregation] + + +@dataclass +class Bucket: + # Field specifies the field used to create buckets on. + field: str + # An integer or float representing the fixed bucket size. + size: Union[int, float] + + +@dataclass +class Table: + buckets: Optional[Bucket] + # Columns contain a series of arrays with the raw result data. + # The columns here line up with the fields in the Fields array. + columns: Optional[List[List[object]]] + # Fields contain information about the fields included in these results. + # The order of the fields match up with the order of the data in Columns. + fields: List[Field] + # Groups specifies which grouping operations has been performed on the + # results. + groups: List[Group] + # Name is the name assigned to this table. Defaults to "0". + # The name "_totals" is reserved for system use. + name: str + # Order echoes the ordering clauses that was used to sort the results. + order: List[Order] + range: Optional[Range] + # Sources contain the names of the datasets that contributed data to these + # results. + sources: List[Source] + + def events(self): + return ColumnIterator(self) + + +class ColumnIterator: + table: Table + i: int = 0 + + def __init__(self, table: Table): + self.table = table + + def __iter__(self): + return self + + def __next__(self): + if ( + self.table.columns is None + or len(self.table.columns) == 0 + or self.i >= len(self.table.columns[0]) + ): + raise StopIteration + + event = {} + for j, f in enumerate(self.table.fields): + event[f.name] = self.table.columns[j][self.i] + + self.i += 1 + return event + + @dataclass class QueryResult: """Result is the result of apl query.""" - request: QueryLegacy + request: Optional[QueryLegacy] # Status of the apl query result. status: QueryStatus # Matches are the events that matched the apl query. - matches: List[Entry] + matches: Optional[List[Entry]] # Buckets are the time series buckets. - buckets: Timeseries + buckets: Optional[Timeseries] + # Tables is populated in tabular queries. + tables: Optional[List[Table]] # Dataset names are the datasets that were used in the apl query. dataset_names: List[str] = field(default_factory=lambda: []) # savedQueryID is the ID of the apl query that generated this result when it diff --git a/src/axiom_py/structlog.py b/src/axiom_py/structlog.py new file mode 100644 index 0000000..41c0285 --- /dev/null +++ b/src/axiom_py/structlog.py @@ -0,0 +1,42 @@ +"""Structlog contains the AxiomProcessor for structlog.""" + +from typing import List +import time +import atexit + +from .client import Client + + +class AxiomProcessor: + """A processor for sending structlogs to Axiom.""" + + client: Client + dataset: str + buffer: List[object] + interval: int + last_run: float + + def __init__(self, client: Client, dataset: str, interval=1): + self.client = client + self.dataset = dataset + self.buffer = [] + self.last_run = time.monotonic() + self.interval = interval + + atexit.register(self._flush) + + def _flush(self): + self.last_run = time.monotonic() + if len(self.buffer) == 0: + return + self.client.ingest_events(self.dataset, self.buffer) + self.buffer = [] + + def __call__(self, logger: object, method_name: str, event_dict: object): + self.buffer.append(event_dict.copy()) + if ( + len(self.buffer) >= 1000 + or time.monotonic() - self.last_run > self.interval + ): + self.flush() + return event_dict diff --git a/axiom/tokens.py b/src/axiom_py/tokens.py similarity index 73% rename from axiom/tokens.py rename to src/axiom_py/tokens.py index fc8fdf3..c74546b 100644 --- a/axiom/tokens.py +++ b/src/axiom_py/tokens.py @@ -17,13 +17,13 @@ class TokenDatasetCapabilities: # Ability to query data. Optional. query: Optional[list[Literal["read"]]] = field(default=None) # Ability to use starred queries. Optional. - starredQueries: Optional[list[Literal["create", "read", "update", "delete"]]] = ( - field(default=None) - ) + starredQueries: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to use virtual fields. Optional. - virtualFields: Optional[list[Literal["create", "read", "update", "delete"]]] = ( - field(default=None) - ) + virtualFields: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) @dataclass @@ -35,53 +35,55 @@ class TokenOrganizationCapabilities: """ # Ability to use annotations. Optional. - annotations: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None - ) + annotations: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to use api tokens. Optional. - apiTokens: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None - ) + apiTokens: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to access billing. Optional. billing: Optional[list[Literal["read", "update"]]] = field(default=None) # Ability to use dashboards. Optional. - dashboards: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None - ) + dashboards: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to use datasets. Optional. - datasets: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None + datasets: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) ) # Ability to use endpoints. Optional. - endpoints: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None - ) + endpoints: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to use flows. Optional. - flows: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None + flows: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) ) # Ability to use integrations. Optional. - integrations: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None - ) + integrations: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to use monitors. Optional. - monitors: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None + monitors: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) ) # Ability to use notifiers. Optional. - notifiers: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None - ) + notifiers: Optional[ + list[Literal["create", "read", "update", "delete"]] + ] = field(default=None) # Ability to use role-based access controls. Optional. - rbac: Optional[list[Literal["create", "read", "update", "delete"]]] = field( - default=None + rbac: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) ) # Ability to use shared access keys. Optional. - sharedAccessKeys: Optional[list[Literal["read", "update"]]] = field(default=None) - # Ability to use users. Optional. - users: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + sharedAccessKeys: Optional[list[Literal["read", "update"]]] = field( default=None ) + # Ability to use users. Optional. + users: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) + ) @dataclass @@ -103,7 +105,9 @@ class TokenAttributes: # Expiration date for the API token. Optional. expiresAt: Optional[str] = field(default=None) # The token's organization-level capabilities. Optional. - orgCapabilities: Optional[TokenOrganizationCapabilities] = field(default=None) + orgCapabilities: Optional[TokenOrganizationCapabilities] = field( + default=None + ) @dataclass diff --git a/src/axiom_py/users.py b/src/axiom_py/users.py new file mode 100644 index 0000000..96f6c0d --- /dev/null +++ b/src/axiom_py/users.py @@ -0,0 +1,44 @@ +from .util import from_dict +from dataclasses import dataclass +from requests import Session +from typing import Optional + + +@dataclass +class Role: + id: str + name: str + + +@dataclass +class User: + """An authenticated axiom user.""" + + id: str + name: str + email: str + role: Role + + +class UsersClient: + """The UsersClient is a client for the Axiom Users service.""" + + has_personal_token: bool + + def __init__(self, session: Session, has_personal_token: bool): + self.session = session + self.has_personal_token = has_personal_token + + def current(self) -> Optional[User]: + """ + Get the current authenticated user. + If your token is not a personal token, this will return None. + + See https://axiom.co/docs/restapi/endpoints/getCurrentUser + """ + if not self.has_personal_token: + return None + + res = self.session.get("/v2/user") + user = from_dict(User, res.json()) + return user diff --git a/src/axiom_py/util.py b/src/axiom_py/util.py new file mode 100644 index 0000000..c9bf296 --- /dev/null +++ b/src/axiom_py/util.py @@ -0,0 +1,75 @@ +import dacite +import iso8601 +from enum import Enum +from uuid import UUID +from typing import Type, TypeVar +from datetime import datetime, timedelta + +from .query import QueryKind +from .query.aggregation import AggregationOperation +from .query.result import MessagePriority +from .query.filter import FilterOperation + + +T = TypeVar("T") + + +def _convert_string_to_datetime(val: str) -> datetime: + d = iso8601.parse_date(val) + return d + + +def _convert_string_to_timedelta(val: str) -> timedelta: + if val == "0": + return timedelta(seconds=0) + + exp = "^([0-9]?)([a-z])$" + import re + + found = re.search(exp, val) + if not found: + raise Exception(f"failed to parse timedelta field from value {val}") + + v = int(found.groups()[0]) + unit = found.groups()[1] + + if unit == "s": + return timedelta(seconds=v) + elif unit == "m": + return timedelta(minutes=v) + elif unit == "h": + return timedelta(hours=v) + elif unit == "d": + return timedelta(days=v) + else: + raise Exception(f"failed to parse timedelta field from value {val}") + + +def from_dict(data_class: Type[T], data) -> T: + cfg = dacite.Config( + type_hooks={ + QueryKind: QueryKind, + datetime: _convert_string_to_datetime, + AggregationOperation: AggregationOperation, + FilterOperation: FilterOperation, + MessagePriority: MessagePriority, + timedelta: _convert_string_to_timedelta, + } + ) + + return dacite.from_dict(data_class=data_class, data=data, config=cfg) + + +def handle_json_serialization(obj): + if isinstance(obj, datetime): + return obj.isoformat("T") + "Z" + elif isinstance(obj, timedelta): + return str(obj.seconds) + "s" + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, UUID): + return str(obj) + + +def is_personal_token(token: str): + return token.startswith("xapt-") diff --git a/src/axiom_py/version.py b/src/axiom_py/version.py new file mode 100644 index 0000000..b4b730c --- /dev/null +++ b/src/axiom_py/version.py @@ -0,0 +1,3 @@ +"""The current version""" + +__version__ = "0.8.0" diff --git a/tests/test_annotations.py b/tests/test_annotations.py index e658372..708d9c5 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -3,14 +3,10 @@ import os import unittest -from typing import List, Dict, Optional from logging import getLogger -from requests.exceptions import HTTPError -from datetime import timedelta from .helpers import get_random_name -from axiom import ( +from axiom_py import ( Client, - DatasetCreateRequest, AnnotationCreateRequest, AnnotationUpdateRequest, ) @@ -32,14 +28,15 @@ def setUpClass(cls): # create dataset cls.dataset_name = get_random_name() - req = DatasetCreateRequest( - name=cls.dataset_name, - description="test_annotations.py (dataset_name)", + cls.client.datasets.create( + cls.dataset_name, "test_annotations.py (dataset_name)" ) - res = cls.client.datasets.create(req) def test_happy_path_crud(self): - """Test the happy path of creating, reading, updating, and deleting an annotation.""" + """ + Test the happy path of creating, reading, updating, and deleting an + annotation. + """ # Create annotation req = AnnotationCreateRequest( datasets=[self.dataset_name], @@ -59,7 +56,9 @@ def test_happy_path_crud(self): assert annotation.id == created_annotation.id # List annotations - annotations = self.client.annotations.list(datasets=[self.dataset_name]) + annotations = self.client.annotations.list( + datasets=[self.dataset_name] + ) self.logger.debug(annotations) assert len(annotations) == 1 @@ -74,7 +73,9 @@ def test_happy_path_crud(self): description=None, url=None, ) - updated_annotation = self.client.annotations.update(annotation.id, updateReq) + updated_annotation = self.client.annotations.update( + annotation.id, updateReq + ) self.logger.debug(updated_annotation) assert updated_annotation.title == newTitle diff --git a/tests/test_client.py b/tests/test_client.py index 42dfd96..1223500 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,7 +1,9 @@ """This module contains the tests for the axiom client.""" +import sys import os import unittest +from unittest.mock import patch import gzip import uuid @@ -12,8 +14,8 @@ from datetime import datetime, timedelta from .helpers import get_random_name -from requests.exceptions import HTTPError -from axiom import ( +from axiom_py import ( + AxiomError, Client, AplOptions, AplResultFormat, @@ -21,11 +23,8 @@ ContentType, IngestOptions, WrongQueryKindException, - DatasetCreateRequest, - TokenAttributes, - TokenOrganizationCapabilities, ) -from axiom.query import ( +from axiom_py.query import ( QueryLegacy, QueryOptions, QueryKind, @@ -37,6 +36,10 @@ Aggregation, AggregationOperation, ) +from axiom_py.tokens import ( + TokenAttributes, + TokenOrganizationCapabilities, +) class TestClient(unittest.TestCase): @@ -51,7 +54,9 @@ def setUpClass(cls): os.getenv("AXIOM_URL"), ) cls.dataset_name = get_random_name() - cls.logger.info(f"generated random dataset name is: {cls.dataset_name}") + cls.logger.info( + f"generated random dataset name is: {cls.dataset_name}" + ) events_time_format = "%d/%b/%Y:%H:%M:%S +0000" # create events to ingest and query time = datetime.utcnow() - timedelta(minutes=1) @@ -80,11 +85,9 @@ def setUpClass(cls): }, ] # create dataset to test the client - req = DatasetCreateRequest( - name=cls.dataset_name, - description="create a dataset to test the python client", + cls.client.datasets.create( + cls.dataset_name, "create a dataset to test the python client" ) - cls.client.datasets.create(req) @responses.activate def test_retries(self): @@ -109,7 +112,8 @@ def test_step001_ingest(self): opts = IngestOptions( "_time", "2/Jan/2006:15:04:05 +0000", - # CSV_delimiter obviously not valid for JSON, but perfectly fine to test for its presence in this test. + # CSV_delimiter obviously not valid for JSON, but perfectly fine to + # test for its presence in this test. ";", ) res = self.client.ingest( @@ -177,6 +181,23 @@ def test_step005_apl_query(self): self.assertEqual(len(qr.matches), len(self.events)) + def test_step005_apl_query_tabular(self): + """Test apl query (tabular)""" + # query the events we ingested in step2 + startTime = datetime.utcnow() - timedelta(minutes=2) + endTime = datetime.utcnow() + + apl = "['%s']" % self.dataset_name + opts = AplOptions( + start_time=startTime, + end_time=endTime, + format=AplResultFormat.Tabular, + ) + qr = self.client.query(apl, opts) + + events = list(qr.tables[0].events()) + self.assertEqual(len(events), len(self.events)) + def test_step005_wrong_query_kind(self): """Test wrong query kind""" startTime = datetime.utcnow() - timedelta(minutes=2) @@ -190,8 +211,10 @@ def test_step005_wrong_query_kind(self): try: self.client.query_legacy(self.dataset_name, q, opts) - except WrongQueryKindException as err: - self.logger.info("passing kind apl to query raised exception as expected") + except WrongQueryKindException: + self.logger.info( + "passing kind apl to query raised exception as expected" + ) return self.fail("was excepting WrongQueryKindException") @@ -201,7 +224,9 @@ def test_step005_complex_query(self): startTime = datetime.utcnow() - timedelta(minutes=2) endTime = datetime.utcnow() aggregations = [ - Aggregation(alias="event_count", op=AggregationOperation.COUNT, field="*") + Aggregation( + alias="event_count", op=AggregationOperation.COUNT, field="*" + ) ] q = QueryLegacy(startTime, endTime, aggregations=aggregations) q.groupBy = ["success", "remote_ip"] @@ -236,6 +261,17 @@ def test_api_tokens(self): # (An exception will be raised if the delete call is not successful.) self.client.delete_api_token(token_values.id) + @patch("sys.exit") + def test_client_shutdown_atexit(self, mock_exit): + """Test client shutdown atexit""" + # Use the mock to test the firing mechanism + self.assertEqual(self.client.is_closed, False) + sys.exit() + mock_exit.assert_called_once() + # Use the hook implementation to assert the client is closed closed + self.client.shutdown_hook() + self.assertEqual(self.client.is_closed, True) + @classmethod def tearDownClass(cls): """A teardown that checks if the dataset still exists and deletes it, @@ -249,7 +285,7 @@ def tearDownClass(cls): "dataset (%s) was not deleted as part of the test, deleting it now." % cls.dataset_name ) - except HTTPError as err: + except AxiomError as e: # nothing to do here, since the dataset doesn't exist - cls.logger.warning(err) + cls.logger.warning(e) cls.logger.info("finish cleaning up after TestClient") diff --git a/tests/test_datasets.py b/tests/test_datasets.py index c7c4c33..59040fd 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -5,14 +5,9 @@ import unittest from typing import List, Dict from logging import getLogger -from requests.exceptions import HTTPError from datetime import timedelta from .helpers import get_random_name -from axiom import ( - Client, - DatasetCreateRequest, - DatasetUpdateRequest, -) +from axiom_py import Client, AxiomError class TestDatasets(unittest.TestCase): @@ -26,7 +21,9 @@ def setUpClass(cls): cls.logger = getLogger() cls.dataset_name = get_random_name() - cls.logger.info(f"generated random dataset name is: {cls.dataset_name}") + cls.logger.info( + f"generated random dataset name is: {cls.dataset_name}" + ) cls.client = Client( os.getenv("AXIOM_TOKEN"), @@ -36,11 +33,9 @@ def setUpClass(cls): def test_step001_create(self): """Tests create dataset endpoint""" - req = DatasetCreateRequest( - name=self.dataset_name, - description="create a dataset to test the python client", + res = self.client.datasets.create( + self.dataset_name, "create a dataset to test the python client" ) - res = self.client.datasets.create(req) self.logger.debug(res) assert res.name == self.dataset_name @@ -60,10 +55,10 @@ def test_step003_list(self): def test_step004_update(self): """Tests update dataset endpoint""" - updateReq = DatasetUpdateRequest("updated name through test") - ds = self.client.datasets.update(self.dataset_name, updateReq) + newDescription = "updated name through test" + ds = self.client.datasets.update(self.dataset_name, newDescription) - assert ds.description == updateReq.description + assert ds.description == newDescription def test_step005_trim(self): """Tests dataset trim endpoint""" @@ -77,13 +72,14 @@ def test_step999_delete(self): dataset = self.client.datasets.get(self.dataset_name) self.assertIsNone( - dataset, f"expected test dataset (%{self.dataset_name}) to be deleted" + dataset, + f"expected test dataset (%{self.dataset_name}) to be deleted", ) - except HTTPError as err: - # the get method returns 404 error if dataset doesn't exist, so that means - # that our tests passed, otherwise, it should fail. - if err.response.status_code != 404: - self.fail(err) + except AxiomError as e: + # the get method returns 404 error if dataset doesn't exist, so + # that means that our tests passed, otherwise, it should fail. + if e.status != 404: + self.fail(e) @classmethod def tearDownClass(cls): @@ -98,7 +94,7 @@ def tearDownClass(cls): "dataset (%s) was not deleted as part of the test, deleting it now." % cls.dataset_name ) - except HTTPError as err: + except AxiomError as e: # nothing to do here, since the dataset doesn't exist - cls.logger.warning(err) + cls.logger.warning(e) cls.logger.info("finish cleaning up after TestDatasets") diff --git a/tests/test_logger.py b/tests/test_logger.py index 7559ad4..e60cc6f 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -4,8 +4,8 @@ import logging import unittest from .helpers import get_random_name -from axiom import Client, DatasetCreateRequest -from axiom.logging import AxiomHandler +from axiom_py import Client +from axiom_py.logging import AxiomHandler class TestLogger(unittest.TestCase): @@ -18,11 +18,9 @@ def test_log(self): ) # create a dataset for that purpose dataset_name = get_random_name() - req = DatasetCreateRequest( - name=dataset_name, - description="a dataset to test axiom-py logger", + client.datasets.create( + dataset_name, "a dataset to test axiom-py logger" ) - client.datasets.create(req) axiom_handler = AxiomHandler(client, dataset_name) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..12e7f5a --- /dev/null +++ b/uv.lock @@ -0,0 +1,568 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "axiom-py" +version = "0.8.0" +source = { editable = "." } +dependencies = [ + { name = "dacite" }, + { name = "iso8601" }, + { name = "ndjson" }, + { name = "pyhumps" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "ujson" }, +] + +[package.dev-dependencies] +dev = [ + { name = "iso8601" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "responses" }, + { name = "rfc3339" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "dacite", specifier = ">=1.8.1" }, + { name = "iso8601", specifier = ">=1.0.2" }, + { name = "ndjson", specifier = ">=0.3.1" }, + { name = "pyhumps", specifier = ">=3.8.0" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "requests-toolbelt", specifier = ">=1.0.0" }, + { name = "ujson", specifier = ">=5.10.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "iso8601", specifier = ">=1.0.2" }, + { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "pytest", specifier = ">=8.3.2" }, + { name = "responses", specifier = ">=0.25.3" }, + { name = "rfc3339", specifier = ">=6.2" }, + { name = "ruff", specifier = ">=0.6.4" }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/ef/d4/a1d72a8f6aa754fdebe91b848912025d30ab7dced61e9ed8aabbf791ed65/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", size = 191415 }, + { url = "https://files.pythonhosted.org/packages/13/82/83c188028b6f38d39538442dd127dc794c602ae6d45d66c469f4063a4c30/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", size = 121051 }, + { url = "https://files.pythonhosted.org/packages/16/ea/a9e284aa38cccea06b7056d4cbc7adf37670b1f8a668a312864abf1ff7c6/charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", size = 119143 }, + { url = "https://files.pythonhosted.org/packages/34/2a/f392457d45e24a0c9bfc012887ed4f3c54bf5d4d05a5deb970ffec4b7fc0/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", size = 137506 }, + { url = "https://files.pythonhosted.org/packages/be/4d/9e370f8281cec2fcc9452c4d1ac513324c32957c5f70c73dd2fa8442a21a/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", size = 147272 }, + { url = "https://files.pythonhosted.org/packages/33/95/ef68482e4a6adf781fae8d183fb48d6f2be8facb414f49c90ba6a5149cd1/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", size = 139734 }, + { url = "https://files.pythonhosted.org/packages/3d/09/d82fe4a34c5f0585f9ea1df090e2a71eb9bb1e469723053e1ee9f57c16f3/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", size = 141094 }, + { url = "https://files.pythonhosted.org/packages/81/b2/160893421adfa3c45554fb418e321ed342bb10c0a4549e855b2b2a3699cb/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", size = 144113 }, + { url = "https://files.pythonhosted.org/packages/9e/ef/cd47a63d3200b232792e361cd67530173a09eb011813478b1c0fb8aa7226/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", size = 138555 }, + { url = "https://files.pythonhosted.org/packages/a8/6f/4ff299b97da2ed6358154b6eb3a2db67da2ae204e53d205aacb18a7e4f34/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", size = 144944 }, + { url = "https://files.pythonhosted.org/packages/d1/2f/0d1efd07c74c52b6886c32a3b906fb8afd2fecf448650e73ecb90a5a27f1/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", size = 148925 }, + { url = "https://files.pythonhosted.org/packages/bd/28/7ea29e73eea52c7e15b4b9108d0743fc9e4cc2cdb00d275af1df3d46d360/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", size = 140732 }, + { url = "https://files.pythonhosted.org/packages/b3/c1/ebca8e87c714a6a561cfee063f0655f742e54b8ae6e78151f60ba8708b3a/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", size = 141288 }, + { url = "https://files.pythonhosted.org/packages/74/20/8923a06f15eb3d7f6a306729360bd58f9ead1dc39bc7ea8831f4b407e4ae/charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", size = 92373 }, + { url = "https://files.pythonhosted.org/packages/db/fb/d29e343e7c57bbf1231275939f6e75eb740cd47a9d7cb2c52ffeb62ef869/charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", size = 99577 }, + { url = "https://files.pythonhosted.org/packages/f7/9d/bcf4a449a438ed6f19790eee543a86a740c77508fbc5ddab210ab3ba3a9a/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", size = 194198 }, + { url = "https://files.pythonhosted.org/packages/66/fe/c7d3da40a66a6bf2920cce0f436fa1f62ee28aaf92f412f0bf3b84c8ad6c/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", size = 122494 }, + { url = "https://files.pythonhosted.org/packages/2a/9d/a6d15bd1e3e2914af5955c8eb15f4071997e7078419328fee93dfd497eb7/charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", size = 120393 }, + { url = "https://files.pythonhosted.org/packages/3d/85/5b7416b349609d20611a64718bed383b9251b5a601044550f0c8983b8900/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", size = 138331 }, + { url = "https://files.pythonhosted.org/packages/79/66/8946baa705c588521afe10b2d7967300e49380ded089a62d38537264aece/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", size = 148097 }, + { url = "https://files.pythonhosted.org/packages/44/80/b339237b4ce635b4af1c73742459eee5f97201bd92b2371c53e11958392e/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", size = 140711 }, + { url = "https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", size = 142251 }, + { url = "https://files.pythonhosted.org/packages/1f/8d/33c860a7032da5b93382cbe2873261f81467e7b37f4ed91e25fed62fd49b/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", size = 144636 }, + { url = "https://files.pythonhosted.org/packages/c2/65/52aaf47b3dd616c11a19b1052ce7fa6321250a7a0b975f48d8c366733b9f/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", size = 139514 }, + { url = "https://files.pythonhosted.org/packages/51/fd/0ee5b1c2860bb3c60236d05b6e4ac240cf702b67471138571dad91bcfed8/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", size = 145528 }, + { url = "https://files.pythonhosted.org/packages/e1/9c/60729bf15dc82e3aaf5f71e81686e42e50715a1399770bcde1a9e43d09db/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", size = 149804 }, + { url = "https://files.pythonhosted.org/packages/53/cd/aa4b8a4d82eeceb872f83237b2d27e43e637cac9ffaef19a1321c3bafb67/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", size = 141708 }, + { url = "https://files.pythonhosted.org/packages/54/7f/cad0b328759630814fcf9d804bfabaf47776816ad4ef2e9938b7e1123d04/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561", size = 142708 }, + { url = "https://files.pythonhosted.org/packages/c1/9d/254a2f1bcb0ce9acad838e94ed05ba71a7cb1e27affaa4d9e1ca3958cdb6/charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", size = 92830 }, + { url = "https://files.pythonhosted.org/packages/2f/0e/d7303ccae9735ff8ff01e36705ad6233ad2002962e8668a970fc000c5e1b/charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", size = 100376 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "dacite" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/0f/cf0943f4f55f0fbc7c6bd60caf1343061dff818b02af5a0d444e473bb78d/dacite-1.8.1-py3-none-any.whl", hash = "sha256:cc31ad6fdea1f49962ea42db9421772afe01ac5442380d9a99fcf3d188c61afe", size = 14309 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "filelock" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/76/3981447fd369539aba35797db99a8e2ff7ed01d9aa63e9344a31658b8d81/filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec", size = 18008 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/95/f9310f35376024e1086c59cbb438d319fc9a4ef853289ce7c661539edbd4/filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609", size = 16170 }, +] + +[[package]] +name = "identify" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/f4/8e8f7db397a7ce20fbdeac5f25adaf567fc362472432938d25556008e03a/identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", size = 99116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/6c/a4f39abe7f19600b74528d0c717b52fff0b300bb0161081510d39c53cb00/identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0", size = 98962 }, +] + +[[package]] +name = "idna" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", size = 189467 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", size = 66894 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "iso8601" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545 }, +] + +[[package]] +name = "ndjson" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/d5/209b6ca94566f9c94c0ec41cee1681c0a3b92a306a84a9b0fcd662088dc3/ndjson-0.3.1.tar.gz", hash = "sha256:bf9746cb6bb1cb53d172cda7f154c07c786d665ff28341e4e689b796b229e5d6", size = 6448 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/c9/04ba0056011ba96a58163ebfd666d8385300bd12da1afe661a5a147758d7/ndjson-0.3.1-py2.py3-none-any.whl", hash = "sha256:839c22275e6baa3040077b83c005ac24199b94973309a8a1809be962c753a410", size = 5305 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a0/d7cab8409cdc7d39b037c85ac46d92434fb6595432e069251b38e5c8dd0e/platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", size = 21276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/8b/d497999c4017b80678017ddce745cf675489c110681ad3c84a55eddfd3e7/platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617", size = 18417 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698 }, +] + +[[package]] +name = "pyhumps" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/83/fa6f8fb7accb21f39e8f2b6a18f76f6d90626bdb0a5e5448e5cc9b8ab014/pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3", size = 9018 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/11/a1938340ecb32d71e47ad4914843775011e6e9da59ba1229f181fef3119e/pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6", size = 6095 }, +] + +[[package]] +name = "pytest" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", size = 1439314 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", size = 341802 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, + { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, + { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, + { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, + { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, + { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, + { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "responses" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/24/1d67c8974daa502e860b4a5b57ad6de0d7dbc0b1160ef7148189a24a40e1/responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba", size = 77798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/24/93293d0be0db9da1ed8dfc5e6af700fdd40e8f10a928704dd179db9f03c1/responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb", size = 55238 }, +] + +[[package]] +name = "rfc3339" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/fb/2835a62f2de226796fce76411daec6b9831eaf6d2fd04994ac1de055dc13/rfc3339-6.2.tar.gz", hash = "sha256:d53c3b5eefaef892b7240ba2a91fef012e86faa4d0a0ca782359c490e00ad4d0", size = 4144 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2e/48d6cf57dec789c90a7b1cb59a21c3cad509f0ec1284632152f33bb1d88d/rfc3339-6.2-py3-none-any.whl", hash = "sha256:f44316b21b21db90a625cde04ebb0d46268f153e6093021fa5893e92a96f58a3", size = 5515 }, +] + +[[package]] +name = "ruff" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/55/9f485266e6326cab707369601b13e3e72eb90ba3eee2d6779549a00a0d58/ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212", size = 2469375 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/78/307591f81d09c8721b5e64539f287c82c81a46f46d16278eb27941ac17f9/ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258", size = 9692673 }, + { url = "https://files.pythonhosted.org/packages/69/63/ef398fcacdbd3995618ed30b5a6c809a1ebbf112ba604b3f5b8c3be464cf/ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60", size = 9481182 }, + { url = "https://files.pythonhosted.org/packages/a6/fd/8784e3bbd79bc17de0a62de05fe5165f494ff7d77cb06630d6428c2f10d2/ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f", size = 9174356 }, + { url = "https://files.pythonhosted.org/packages/6d/bc/c69db2d68ac7bfbb222c81dc43a86e0402d0063e20b13e609f7d17d81d3f/ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc", size = 10129365 }, + { url = "https://files.pythonhosted.org/packages/3b/10/8ed14ff60a4e5eb08cac0a04a9b4e8590c72d1ce4d29ef22cef97d19536d/ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617", size = 9483351 }, + { url = "https://files.pythonhosted.org/packages/a9/69/13316b8d64ffd6a43627cf0753339a7f95df413450c301a60904581bee6e/ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408", size = 10301099 }, + { url = "https://files.pythonhosted.org/packages/42/00/9623494087272643e8f02187c266638306c6829189a5bf1446968bbe438b/ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e", size = 11033216 }, + { url = "https://files.pythonhosted.org/packages/c5/31/e0c9d881db42ea1267e075c29aafe0db5a8a3024b131f952747f6234f858/ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818", size = 10618140 }, + { url = "https://files.pythonhosted.org/packages/5b/35/f1d8b746aedd4c8fde4f83397e940cc4c8fc619860ebbe3073340381a34d/ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6", size = 11606672 }, + { url = "https://files.pythonhosted.org/packages/c5/70/899b03cbb3eb48ed0507d4b32b6f7aee562bc618ef9ffda855ec98c0461a/ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa", size = 10288013 }, + { url = "https://files.pythonhosted.org/packages/17/c6/906bf895640521ca5115ccdd857b2bac42bd61facde6620fdc2efc0a4806/ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6", size = 10109473 }, + { url = "https://files.pythonhosted.org/packages/28/da/1284eb04172f8a5d42eb52fce9d643dd747ac59a4ed6c5d42729f72e934d/ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d", size = 9568817 }, + { url = "https://files.pythonhosted.org/packages/6c/e2/f8250b54edbb2e9222e22806e1bcc35a192ac18d1793ea556fa4977a843a/ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa", size = 9910840 }, + { url = "https://files.pythonhosted.org/packages/9c/7c/dcf2c10562346ecdf6f0e5f6669b2ddc9a74a72956c3f419abd6820c2aff/ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1", size = 10354263 }, + { url = "https://files.pythonhosted.org/packages/f1/94/c39d7ac5729e94788110503d928c98c203488664b0fb92c2b801cb832bec/ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523", size = 7958602 }, + { url = "https://files.pythonhosted.org/packages/6b/d2/2dee8c547bee3d4cfdd897f7b8e38510383acaff2c8130ea783b67631d72/ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58", size = 8795059 }, + { url = "https://files.pythonhosted.org/packages/07/1a/23280818aa4fa89bd0552aab10857154e1d3b90f27b5b745f09ec1ac6ad8/ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14", size = 8239636 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "ujson" +version = "5.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/91/91678e49a9194f527e60115db84368c237ac7824992224fac47dcb23a5c6/ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd", size = 55354 }, + { url = "https://files.pythonhosted.org/packages/de/2f/1ed8c9b782fa4f44c26c1c4ec686d728a4865479da5712955daeef0b2e7b/ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf", size = 51808 }, + { url = "https://files.pythonhosted.org/packages/51/bf/a3a38b2912288143e8e613c6c4c3f798b5e4e98c542deabf94c60237235f/ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6", size = 51995 }, + { url = "https://files.pythonhosted.org/packages/b4/6d/0df8f7a6f1944ba619d93025ce468c9252aa10799d7140e07014dfc1a16c/ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569", size = 53566 }, + { url = "https://files.pythonhosted.org/packages/d5/ec/370741e5e30d5f7dc7f31a478d5bec7537ce6bfb7f85e72acefbe09aa2b2/ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770", size = 58499 }, + { url = "https://files.pythonhosted.org/packages/fe/29/72b33a88f7fae3c398f9ba3e74dc2e5875989b25f1c1f75489c048a2cf4e/ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1", size = 997881 }, + { url = "https://files.pythonhosted.org/packages/70/5c/808fbf21470e7045d56a282cf5e85a0450eacdb347d871d4eb404270ee17/ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5", size = 1140631 }, + { url = "https://files.pythonhosted.org/packages/8f/6a/e1e8281408e6270d6ecf2375af14d9e2f41c402ab6b161ecfa87a9727777/ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51", size = 1043511 }, + { url = "https://files.pythonhosted.org/packages/cb/ca/e319acbe4863919ec62498bc1325309f5c14a3280318dca10fe1db3cb393/ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518", size = 38626 }, + { url = "https://files.pythonhosted.org/packages/78/ec/dc96ca379de33f73b758d72e821ee4f129ccc32221f4eb3f089ff78d8370/ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f", size = 42076 }, + { url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353 }, + { url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813 }, + { url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988 }, + { url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561 }, + { url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497 }, + { url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877 }, + { url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632 }, + { url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513 }, + { url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616 }, + { url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071 }, + { url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 }, + { url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 }, + { url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 }, + { url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 }, + { url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 }, + { url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 }, + { url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 }, + { url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 }, + { url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 }, + { url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 }, + { url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 }, + { url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 }, + { url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 }, + { url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 }, + { url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 }, + { url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 }, + { url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 }, + { url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 }, + { url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 }, + { url = "https://files.pythonhosted.org/packages/01/9c/2387820623455ac81781352e095a119250a9f957717490ad57957d875e56/ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050", size = 55490 }, + { url = "https://files.pythonhosted.org/packages/b7/8d/0902429667065ee1a30f400ff4f0e97f1139fc958121856d520c35da3d1e/ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd", size = 51886 }, + { url = "https://files.pythonhosted.org/packages/6e/07/41145ed78838385ded3aceedb1bae496e7fb1c558fcfa337fd51651d0ec5/ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb", size = 52022 }, + { url = "https://files.pythonhosted.org/packages/ef/6a/5c383afd4b099771fe9ad88699424a0f405f65543b762500e653244d5d04/ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a", size = 53610 }, + { url = "https://files.pythonhosted.org/packages/ba/17/940791e0a5fb5e90c2cd44fded53eb666b833918b5e65875dbd3e10812f9/ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d", size = 58567 }, + { url = "https://files.pythonhosted.org/packages/03/b4/9be6bc48b8396983fa013a244e2f9fc1defcc0c4c55f76707930e749ad14/ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe", size = 998051 }, + { url = "https://files.pythonhosted.org/packages/66/0b/d3620932fe5619b51cd05162b7169be2158bde88493d6fa9caad46fefb0b/ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7", size = 1140680 }, + { url = "https://files.pythonhosted.org/packages/f5/cb/475defab49cac018d34ac7d47a2d5c8d764484ce8831d8fa8f523c41349d/ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4", size = 1043571 }, + { url = "https://files.pythonhosted.org/packages/15/87/a256f829e32fbb2b0047b6dac260386f75591d17d5914b25ddc3c284d5b4/ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8", size = 38653 }, + { url = "https://files.pythonhosted.org/packages/d6/28/55e3890f814727aa984f66effa5e3e848863777409e96183c59e15152f73/ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc", size = 42132 }, + { url = "https://files.pythonhosted.org/packages/97/94/50ff2f1b61d668907f20216873640ab19e0eaa77b51e64ee893f6adfb266/ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b", size = 55421 }, + { url = "https://files.pythonhosted.org/packages/0c/b3/3d2ca621d8dbeaf6c5afd0725e1b4bbd465077acc69eff1e9302735d1432/ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27", size = 51816 }, + { url = "https://files.pythonhosted.org/packages/8d/af/5dc103cb4d08f051f82d162a738adb9da488d1e3fafb9fd9290ea3eabf8e/ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76", size = 52023 }, + { url = "https://files.pythonhosted.org/packages/5d/dd/b9a6027ba782b0072bf24a70929e15a58686668c32a37aebfcfaa9e00bdd/ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5", size = 53622 }, + { url = "https://files.pythonhosted.org/packages/1f/28/bcf6df25c1a9f1989dc2ddc4ac8a80e246857e089f91a9079fd8a0a01459/ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0", size = 58563 }, + { url = "https://files.pythonhosted.org/packages/9e/82/89404453a102d06d0937f6807c0a7ef2eec68b200b4ce4386127f3c28156/ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1", size = 998050 }, + { url = "https://files.pythonhosted.org/packages/63/eb/2a4ea07165cad217bc842bb684b053bafa8ffdb818c47911c621e97a33fc/ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1", size = 1140672 }, + { url = "https://files.pythonhosted.org/packages/72/53/d7bdf6afabeba3ed899f89d993c7f202481fa291d8c5be031c98a181eda4/ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996", size = 1043577 }, + { url = "https://files.pythonhosted.org/packages/19/b1/75f5f0d18501fd34487e46829de3070724c7b350f1983ba7f07e0986720b/ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9", size = 38654 }, + { url = "https://files.pythonhosted.org/packages/77/0d/50d2f9238f6d6683ead5ecd32d83d53f093a3c0047ae4c720b6d586cb80d/ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a", size = 42134 }, + { url = "https://files.pythonhosted.org/packages/95/53/e5f5e733fc3525e65f36f533b0dbece5e5e2730b760e9beacf7e3d9d8b26/ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64", size = 51846 }, + { url = "https://files.pythonhosted.org/packages/59/1f/f7bc02a54ea7b47f3dc2d125a106408f18b0f47b14fc737f0913483ae82b/ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3", size = 48103 }, + { url = "https://files.pythonhosted.org/packages/1a/3a/d3921b6f29bc744d8d6c56db5f8bbcbe55115fd0f2b79c3c43ff292cc7c9/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a", size = 47257 }, + { url = "https://files.pythonhosted.org/packages/f1/04/f4e3883204b786717038064afd537389ba7d31a72b437c1372297cb651ea/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746", size = 48468 }, + { url = "https://files.pythonhosted.org/packages/17/cd/9c6547169eb01a22b04cbb638804ccaeb3c2ec2afc12303464e0f9b2ee5a/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88", size = 54266 }, + { url = "https://files.pythonhosted.org/packages/70/bf/ecd14d3cf6127f8a990b01f0ad20e257f5619a555f47d707c57d39934894/ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b", size = 42224 }, + { url = "https://files.pythonhosted.org/packages/c2/6d/749c8349ad080325d9dbfabd7fadfa79e4bb8304e9e0f2c42f0419568328/ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337", size = 51849 }, + { url = "https://files.pythonhosted.org/packages/32/56/c8be7aa5520b96ffca82ab77112429fa9ed0f805cd33ad3ab3e6fe77c6e6/ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1", size = 48091 }, + { url = "https://files.pythonhosted.org/packages/a1/d7/27727f4de9f79f7be3e294f08d0640c4bba4c40d716a1523815f3d161e44/ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753", size = 48488 }, + { url = "https://files.pythonhosted.org/packages/45/9c/168928f96be009b93161eeb19cd7e058c397a6f79daa76667a2f26a6d775/ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6", size = 54278 }, + { url = "https://files.pythonhosted.org/packages/bd/0b/67770fc8eb6c8d1ecabe3f9dec937bc59611028e41dc0ff9febb582976db/ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5", size = 42282 }, + { url = "https://files.pythonhosted.org/packages/8d/96/a3a2356ca5a4b67fe32a0c31e49226114d5154ba2464bb1220a93eb383e8/ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4", size = 51855 }, + { url = "https://files.pythonhosted.org/packages/73/3d/41e78e7500e75eb6b5a7ab06907a6df35603b92ac6f939b86f40e9fe2c06/ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8", size = 48059 }, + { url = "https://files.pythonhosted.org/packages/be/14/e435cbe5b5189483adbba5fe328e88418ccd54b2b1f74baa4172384bb5cd/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b", size = 47238 }, + { url = "https://files.pythonhosted.org/packages/e8/d9/b6f4d1e6bec20a3b582b48f64eaa25209fd70dc2892b21656b273bc23434/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804", size = 48457 }, + { url = "https://files.pythonhosted.org/packages/23/1c/cfefabb5996e21a1a4348852df7eb7cfc69299143739e86e5b1071c78735/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e", size = 54238 }, + { url = "https://files.pythonhosted.org/packages/af/c4/fa70e77e1c27bbaf682d790bd09ef40e86807ada704c528ef3ea3418d439/ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7", size = 42230 }, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/8a/134f65c3d6066153b84fc176c58877acd8165ed0b79a149ff50502597284/virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c", size = 9385017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ea/12f774a18b55754c730c8383dad8f10d7b87397d1cb6b2b944c87381bb3b/virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55", size = 6013327 }, +]