├── fhir_py_types ├── reader │ ├── __init__.py │ └── bundle.py ├── __init__.py ├── cli.py ├── header.py.tpl └── ast.py ├── generated └── .gitignore ├── spec ├── r4.bundle.checksumfile ├── r5.bundle.checksumfile ├── .gitignore └── download_spec_bundle.sh ├── regression └── synthea │ ├── r4.bundle.checksumfile │ ├── .gitignore │ ├── download_sample_bundle.sh │ └── test_synthea_samples.py ├── compose.yaml ├── ruff.toml ├── Dockerfile ├── pyproject.toml ├── LICENSE ├── README.md ├── .gitignore ├── .github └── workflows │ └── static-code-analysis-and-tests.yml ├── examples └── fhirpy.ipynb ├── tests └── test_ast.py └── poetry.lock /fhir_py_types/reader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /spec/r4.bundle.checksumfile: -------------------------------------------------------------------------------- 1 | a2793a06853c2d4540db8a72fc1c6d972528b01d113c2bb70ae2d80dc062e963 definitions.json.zip 2 | -------------------------------------------------------------------------------- /spec/r5.bundle.checksumfile: -------------------------------------------------------------------------------- 1 | df0d7259b4a8741d59f4971d96dd486423ecbd414c7060e9dc006ae3c3209c0c definitions.json.zip 2 | -------------------------------------------------------------------------------- /regression/synthea/r4.bundle.checksumfile: -------------------------------------------------------------------------------- 1 | a6fc595d9c0f4c646746af42f861b5a12d03c856af158dd837c764dfb81b66f8 r4.bundle.zip 2 | -------------------------------------------------------------------------------- /regression/synthea/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !r4.bundle.checksumfile 4 | !download_sample_bundle.sh 5 | !test_synthea_samples.py 6 | -------------------------------------------------------------------------------- /spec/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !bundle.checksumfile 4 | !r4.bundle.checksumfile 5 | !r5.bundle.checksumfile 6 | !download_spec_bundle.sh 7 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | typegen: 3 | build: . 4 | volumes: 5 | - ./fhir_py_types:/app/fhir_py_types 6 | - ./spec:/app/spec 7 | - ./generated:/app/generated 8 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # Visit https://beta.ruff.rs/docs/rules/ for details 2 | target-version = "py312" 3 | lint.select = ["E", "F", "N", "UP", "ANN", "B", "C4", "PT", "TD", "I001"] 4 | lint.ignore = ["E501"] 5 | -------------------------------------------------------------------------------- /regression/synthea/download_sample_bundle.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | function download_r4() { 4 | curl --output r4.bundle.zip https://synthetichealth.github.io/synthea-sample-data/downloads/synthea_sample_data_fhir_r4_sep2019.zip 5 | shasum --algorithm 256 --check r4.bundle.checksumfile 6 | unzip r4.bundle.zip 7 | } 8 | 9 | case "$1" in 10 | *) 11 | download_r4 12 | ;; 13 | esac 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | RUN addgroup --gid 1000 dockeruser 4 | RUN adduser --disabled-login --uid 1000 --gid 1000 dockeruser 5 | RUN mkdir -p /app/spec 6 | RUN chown -R dockeruser:dockeruser /app/ 7 | 8 | RUN pip install poetry 9 | 10 | USER dockeruser 11 | 12 | COPY pyproject.toml poetry.lock /app/ 13 | COPY fhir_py_types /app/fhir_py_types 14 | WORKDIR /app 15 | 16 | RUN poetry install 17 | 18 | CMD ["poetry", "run", "typegen", "--from-bundles", "/app/spec/fhir.types.json", "--from-bundles", "/app/spec/fhir.resources.json", "--outfile", "/app/generated/resources.py"] 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fhir-py-types" 3 | version = "0.3.1" 4 | description = "Convert FHIR StructureDefinition into Python type annotations" 5 | authors = ["beda.software "] 6 | packages = [{include = "fhir_py_types"}] 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | pydantic = "^2.9.0" 11 | 12 | [tool.poetry.group.dev.dependencies] 13 | black = {version = "^23.1a1", allow-prereleases = true} 14 | pytest = "^7.2.1" 15 | mypy = "^1.0.0" 16 | ruff = "^0.5.5" 17 | autohooks = "^23.7.0" 18 | autohooks-plugin-black = "^23.7.0" 19 | autohooks-plugin-mypy = "^23.3.0" 20 | autohooks-plugin-ruff = "^23.6.1" 21 | 22 | [tool.autohooks] 23 | mode = "poetry" 24 | pre-commit = ["autohooks.plugins.black", "autohooks.plugins.mypy", "autohooks.plugins.ruff"] 25 | 26 | [tool.poetry.scripts] 27 | typegen = "fhir_py_types.cli:main" 28 | 29 | [build-system] 30 | requires = ["poetry-core"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /regression/synthea/test_synthea_samples.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | from collections.abc import Generator 5 | 6 | import pytest 7 | 8 | from generated.resources import Bundle 9 | 10 | NUMBER_OF_SAMPLES_TO_VALIDATE = 10 11 | 12 | 13 | def iterate_synthea_bundles() -> Generator[str]: 14 | directory = os.path.join(os.path.dirname(__file__), "./fhir/") 15 | directory_content = random.sample( 16 | [path for path in os.listdir(directory) if path.endswith(".json")], 17 | NUMBER_OF_SAMPLES_TO_VALIDATE, 18 | ) 19 | for filename in directory_content: 20 | yield os.path.join(directory, filename) 21 | 22 | 23 | @pytest.mark.parametrize("bundle_filepath", iterate_synthea_bundles()) 24 | def test_can_parse_and_validate_samples_bundle(bundle_filepath: str) -> None: 25 | with open(bundle_filepath, "rb") as bundle_file: 26 | original = json.loads(bundle_file.read()) 27 | bundle = Bundle.model_validate(original) 28 | assert bundle.model_dump() == original 29 | -------------------------------------------------------------------------------- /spec/download_spec_bundle.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | function download_r4() { 4 | curl --output definitions.json.zip https://hl7.org/fhir/R4B/definitions.json.zip 5 | shasum --algorithm 256 --check r4.bundle.checksumfile 6 | unzip definitions.json.zip "definitions.json/profiles-types.json" "definitions.json/profiles-resources.json" 7 | mv -v definitions.json/profiles-types.json fhir.types.json 8 | mv -v definitions.json/profiles-resources.json fhir.resources.json 9 | 10 | rm -rf definitions.json/ 11 | rm definitions.json.zip 12 | } 13 | 14 | function download_r5() { 15 | curl --output definitions.json.zip https://hl7.org/fhir/R5/definitions.json.zip 16 | shasum --algorithm 256 --check r5.bundle.checksumfile 17 | unzip definitions.json.zip "profiles-types.json" "profiles-resources.json" 18 | mv -v profiles-types.json fhir.types.json 19 | mv -v profiles-resources.json fhir.resources.json 20 | 21 | rm definitions.json.zip 22 | } 23 | 24 | case "$1" in 25 | "r5") 26 | download_r5 27 | ;; 28 | *) 29 | download_r4 30 | ;; 31 | esac 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, beda.software 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://github.com/beda-software/fhir-py-types/actions/workflows/static-code-analysis-and-tests.yml/badge.svg)](https://github.com/beda-software/fhir-py-types/actions/workflows/static-code-analysis-and-tests.yml) 2 | [![Supported Python version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/release/python-3110/) 3 | 4 | ## Quick start 5 | 6 | Download `StructureDefinition` bundle and build type definitions from it: 7 | 8 | ```sh 9 | git clone --depth 1 https://github.com/beda-software/fhir-py-types.git 10 | cd fhir-py-types/spec/ 11 | sh download_spec_bundle.sh 12 | cd .. 13 | docker compose up 14 | ``` 15 | 16 | The generated type definitions can then be found in `generated/resources.py`. 17 | 18 | ## How it works 19 | 20 | The build process is based on the standard `StructureDefintion` resource (available [in JSON format](https://hl7.org/fhir/downloads.html) from the FHIR download page, [direct link](https://hl7.org/fhir/definitions.json.zip) at the time of writing). 21 | 22 | ## Contributing 23 | 24 | The project uses [poetry](https://github.com/python-poetry/poetry) for package management. 25 | 26 | Type definitions can be generated by running: 27 | 28 | ```sh 29 | poetry install 30 | poetry run typegen --from-bundles spec/fhir.types.json --from-bundles spec/fhir.resources.json --outfile generated/resources.py 31 | ``` 32 | 33 | Where `spec/fhir.types.json` and `spec/fhir.resources.json` are bundles of `StructureDefinition` resources. 34 | 35 | Type check definitions (the very first type checking process might take a while to complete, consecutive runs should be faster) 36 | 37 | ```sh 38 | poetry run mypy generated/resources.py 39 | ``` 40 | -------------------------------------------------------------------------------- /fhir_py_types/__init__.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | class StructureDefinitionKind(Enum): 6 | PRIMITIVE = "primitive-type" 7 | COMPLEX = "complex-type" 8 | CAPABILITY = "capability" 9 | OPERATION = "operation" 10 | RESOURCE = "resource" 11 | 12 | @staticmethod 13 | def from_str(kind: str) -> "StructureDefinitionKind": 14 | match kind: 15 | case "primitive-type": 16 | return StructureDefinitionKind.PRIMITIVE 17 | case "complex-type": 18 | return StructureDefinitionKind.COMPLEX 19 | case "capability": 20 | return StructureDefinitionKind.CAPABILITY 21 | case "operation": 22 | return StructureDefinitionKind.OPERATION 23 | case "resource": 24 | return StructureDefinitionKind.RESOURCE 25 | case _: 26 | raise ValueError(f"Unknown StructureDefinition kind: {kind}") 27 | 28 | 29 | @dataclass(frozen=True) 30 | class StructurePropertyType: 31 | code: str 32 | required: bool = False 33 | isarray: bool = False 34 | literal: bool = False 35 | target_profile: list[str] | None = None 36 | alias: str | None = None 37 | 38 | 39 | @dataclass(frozen=True) 40 | class StructureDefinition: 41 | id: str 42 | docstring: str 43 | type: list[StructurePropertyType] 44 | elements: dict[str, "StructureDefinition"] 45 | kind: StructureDefinitionKind | None = None 46 | 47 | 48 | def is_polymorphic(definition: StructureDefinition) -> bool: 49 | return len(definition.type) > 1 50 | 51 | 52 | def is_primitive_type(property_type: StructurePropertyType) -> bool: 53 | # All primitive types starts with lowercased letters 54 | return property_type.code[0].islower() 55 | -------------------------------------------------------------------------------- /fhir_py_types/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import ast 3 | import itertools 4 | import logging 5 | import os 6 | 7 | from fhir_py_types.ast import build_ast 8 | from fhir_py_types.reader.bundle import load_from_bundle 9 | 10 | logging.basicConfig(level=logging.DEBUG) 11 | logger = logging.getLogger(__name__) 12 | 13 | dir_path = os.path.dirname(os.path.realpath(__file__)) 14 | 15 | 16 | def main() -> None: 17 | argparser = argparse.ArgumentParser( 18 | description="Generate Python typed data models from FHIR resources definition" 19 | ) 20 | argparser.add_argument( 21 | "--from-bundles", 22 | action="append", 23 | required=True, 24 | help="File path to read 'StructureDefinition' resources from (repeat to read multiple files)", 25 | ) 26 | argparser.add_argument( 27 | "--outfile", 28 | required=True, 29 | help="File path to write generated Python typed data models to", 30 | ) 31 | argparser.add_argument( 32 | "--base-model", 33 | default="pydantic.BaseModel", 34 | help="Python path to the Base Model class to use as the base class for generated models", 35 | ) 36 | args = argparser.parse_args() 37 | 38 | ast_ = build_ast( 39 | itertools.chain.from_iterable( 40 | load_from_bundle(bundle) for bundle in args.from_bundles 41 | ) 42 | ) 43 | with open(os.path.join(dir_path, "header.py.tpl")) as header_file: 44 | header_lines = header_file.readlines() 45 | 46 | with open(os.path.abspath(args.outfile), "w") as resource_file: 47 | resource_file.writelines( 48 | [ 49 | *header_lines, 50 | "\n\n", 51 | "\n\n\n".join( 52 | ast.unparse(ast.fix_missing_locations(tree)) for tree in ast_ 53 | ), 54 | ] 55 | ) 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # VSCode 132 | .vscode/ 133 | -------------------------------------------------------------------------------- /.github/workflows/static-code-analysis-and-tests.yml: -------------------------------------------------------------------------------- 1 | name: Static Code Analysis and Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | run-static-code-analysis: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 3.12 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: "3.12" 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install poetry 23 | poetry install 24 | - name: Run static code analysis with Ruff 25 | run: poetry run ruff check fhir_py_types/ tests/ 26 | - name: Run typecheck with Mypy 27 | run: poetry run mypy fhir_py_types/ tests/ 28 | run-unit-tests: 29 | runs-on: ubuntu-latest 30 | needs: [run-static-code-analysis] 31 | steps: 32 | - uses: actions/checkout@v3 33 | - name: Set up Python 3.12 34 | uses: actions/setup-python@v3 35 | with: 36 | python-version: "3.12" 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install poetry 41 | poetry install 42 | - name: Test with pytest 43 | run: poetry run pytest tests/ 44 | run-spec-regression-tests: 45 | runs-on: ubuntu-latest 46 | needs: [run-unit-tests] 47 | strategy: 48 | matrix: 49 | spec_version: ["r4", "r5"] 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: Set up Python 3.12 53 | uses: actions/setup-python@v3 54 | with: 55 | python-version: "3.12" 56 | - name: Install dependencies 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install poetry 60 | poetry install 61 | - name: Cache spec StructureDefinition bundle 62 | id: spec-download 63 | uses: actions/cache@v3 64 | with: 65 | key: ${{ runner.os }}-spec-download-${{ hashFiles(format('./spec/{0}.bundle.checksumfile', matrix.spec_version)) }} 66 | path: | 67 | ./spec/fhir.types.json 68 | ./spec/fhir.resources.json 69 | - name: Download spec StructureDefinition bundle 70 | if: steps.spec-download.outputs.cache-hit != 'true' 71 | run: | 72 | cd ./spec/ 73 | bash download_spec_bundle.sh ${{ matrix.spec_version }} 74 | cd .. 75 | - name: Build resource models from the spec StructureDefinition bundle 76 | run: | 77 | poetry run typegen --from-bundles spec/fhir.types.json --from-bundles spec/fhir.resources.json --outfile generated/resources.py 78 | - name: Typecheck generated resource models 79 | run: poetry run mypy generated/resources.py 80 | run-synthea-models-regression-tests: 81 | runs-on: ubuntu-latest 82 | needs: [run-spec-regression-tests] 83 | steps: 84 | - uses: actions/checkout@v3 85 | - name: Set up Python 3.12 86 | uses: actions/setup-python@v3 87 | with: 88 | python-version: "3.12" 89 | - name: Install dependencies 90 | run: | 91 | python -m pip install --upgrade pip 92 | pip install poetry 93 | poetry install 94 | - name: Cache spec generated resource models 95 | id: spec-download 96 | uses: actions/cache@v3 97 | with: 98 | key: ${{ runner.os }}-spec-generated-${{ hashFiles('./spec/r4.bundle.checksumfile') }} 99 | path: ./generated/resources.py 100 | - name: Download R4 spec and build resource models 101 | if: steps.spec-download.outputs.cache-hit != 'true' 102 | run: | 103 | cd ./spec/ 104 | bash download_spec_bundle.sh 105 | cd .. 106 | poetry run typegen --from-bundles spec/fhir.types.json --from-bundles spec/fhir.resources.json --outfile generated/resources.py 107 | - name: Cache Synthea samples 108 | id: synthea-samples-download 109 | uses: actions/cache@v3 110 | with: 111 | key: ${{ runner.os }}-synthea-samples-download-${{ hashFiles('./regression/synthea/r4.bundle.checksumfile') }} 112 | path: ./regression/synthea/fhir/ 113 | - name: Download Synthea samples 114 | if: steps.synthea-samples-download.outputs.cache-hit != 'true' 115 | run: | 116 | cd ./regression/synthea/ 117 | bash download_sample_bundle.sh 118 | cd ../../ 119 | - name: Test Synthea samples can be parsed 120 | run: poetry run pytest regression/synthea/test_synthea_samples.py -vv 121 | -------------------------------------------------------------------------------- /fhir_py_types/header.py.tpl: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | List as List_, 3 | Optional as Optional_, 4 | Literal as Literal_, 5 | Any as Any_, 6 | ) 7 | 8 | from pydantic import ( 9 | BaseModel as BaseModel_, 10 | ConfigDict, 11 | Field, 12 | SerializationInfo, 13 | field_validator, 14 | field_serializer, 15 | ValidationError, 16 | ) 17 | from pydantic.main import IncEx 18 | from pydantic_core import PydanticCustomError 19 | 20 | 21 | class AnyResource(BaseModel_): 22 | model_config = ConfigDict(extra="allow") 23 | 24 | resourceType: str 25 | id: Optional_[str] = None 26 | 27 | 28 | class BaseModel(BaseModel_): 29 | model_config = ConfigDict( 30 | # Extra attributes are disabled because fhir does not allow it 31 | extra="forbid", 32 | # Validation are applied while mutating the resource 33 | validate_assignment=True, 34 | # It's important for reserved keywords population in constructor (e.g. for_) 35 | populate_by_name=True, 36 | # Speed up initial load by lazy build 37 | defer_build=True, 38 | # It does not break anything, just for convinience 39 | coerce_numbers_to_str=True, 40 | ) 41 | 42 | def model_dump( 43 | self, 44 | *, 45 | mode: Literal_["json", "python"] | str = "python", 46 | include: IncEx = None, 47 | exclude: IncEx = None, 48 | context: Any_ | None = None, 49 | by_alias: bool = True, 50 | exclude_unset: bool = False, 51 | exclude_defaults: bool = False, 52 | exclude_none: bool = True, 53 | round_trip: bool = False, 54 | warnings: bool | Literal_["none", "warn", "error"] = True, 55 | serialize_as_any: bool = False, 56 | ): 57 | # Override default parameters for by_alias and exclude_none preserving function declaration 58 | return super().model_dump( 59 | mode=mode, 60 | include=include, 61 | exclude=exclude, 62 | context=context, 63 | by_alias=by_alias, 64 | exclude_unset=exclude_unset, 65 | exclude_defaults=exclude_defaults, 66 | exclude_none=exclude_none, 67 | round_trip=round_trip, 68 | warnings=warnings, 69 | serialize_as_any=serialize_as_any, 70 | ) 71 | 72 | @field_serializer("*") 73 | @classmethod 74 | def serialize_all_fields(cls, value: Any_, info: SerializationInfo): 75 | if isinstance(value, list): 76 | return [_serialize(v, info) for v in value] 77 | 78 | return _serialize(value, info) 79 | 80 | @field_validator("*") 81 | @classmethod 82 | def validate_all_fields(cls, value: Any_): 83 | if isinstance(value, list): 84 | return [_validate(v, index=index) for index, v in enumerate(value)] 85 | return _validate(value) 86 | 87 | 88 | def _serialize(value: Any_, info: SerializationInfo): 89 | # Custom serializer for AnyResource fields 90 | kwargs = { 91 | "mode": info.mode, 92 | "include": info.include, 93 | "exclude": info.exclude, 94 | "context": info.context, 95 | "by_alias": info.by_alias, 96 | "exclude_unset": info.exclude_unset, 97 | "exclude_defaults": info.exclude_defaults, 98 | "exclude_none": info.exclude_none, 99 | "round_trip": info.round_trip, 100 | "serialize_as_any": info.serialize_as_any, 101 | } 102 | if isinstance(value, AnyResource): 103 | return value.model_dump(**kwargs) # type: ignore 104 | if isinstance(value, BaseModel_): 105 | return value.model_dump(**kwargs) # type: ignore 106 | return value 107 | 108 | 109 | def _validate(value: Any_, index: int | None = None): 110 | # Custom validator for AnyResource fields 111 | if isinstance(value, AnyResource): 112 | try: 113 | klass = globals()[value.resourceType] 114 | except KeyError as exc: 115 | raise ValidationError.from_exception_data( 116 | "ImportError", 117 | [ 118 | { 119 | "loc": (index, "resourceType") 120 | if index is not None 121 | else ("resourceType",), 122 | "type": "value_error", 123 | "input": [value], 124 | "ctx": {"error": f"{value.resourceType} resource is not found"}, 125 | } 126 | ], 127 | ) from exc 128 | 129 | if not issubclass(klass, BaseModel) or "resourceType" not in klass.model_fields: 130 | raise ValidationError.from_exception_data( 131 | "ImportError", 132 | [ 133 | { 134 | "loc": (index, "resourceType") 135 | if index is not None 136 | else ("resourceType",), 137 | "type": "value_error", 138 | "input": [value], 139 | "ctx": {"error": f"{value.resourceType} is not a resource"}, 140 | } 141 | ], 142 | ) 143 | 144 | try: 145 | return klass(**value.model_dump()) 146 | except ValidationError as exc: 147 | raise ValidationError.from_exception_data( 148 | exc.title, 149 | [ 150 | { 151 | "loc": (index, *error["loc"]) 152 | if index is not None 153 | else error["loc"], 154 | "type": error["type"], 155 | "input": error["input"], 156 | "ctx": error["ctx"] 157 | } 158 | for error in exc.errors() 159 | ], 160 | ) from exc 161 | 162 | return value -------------------------------------------------------------------------------- /fhir_py_types/reader/bundle.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from collections.abc import Iterable 5 | from typing import Any 6 | 7 | from fhir_py_types import ( 8 | StructureDefinition, 9 | StructureDefinitionKind, 10 | StructurePropertyType, 11 | ) 12 | 13 | FHIR_TO_SYSTEM_TYPE_MAP = { 14 | "System.String": "str", 15 | "System.Boolean": "bool", 16 | "System.Time": "str", 17 | "System.Date": "str", 18 | "System.DateTime": "str", 19 | "System.Decimal": "float", 20 | "System.Integer": "int", 21 | } 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | DefinitionsBundle = dict[str, Any] 27 | 28 | 29 | def parse_type_identifier(type_: str) -> str: 30 | code = type_.split("/")[-1] 31 | return FHIR_TO_SYSTEM_TYPE_MAP.get(code, code) 32 | 33 | 34 | def parse_target_profile(target_profile: list[str]) -> list[str]: 35 | profiles = [p.split("/")[-2:] for p in target_profile] 36 | if any(type_ != "StructureDefinition" for type_, _ in profiles): 37 | raise ValueError(f"Unknown target profile type: {target_profile}") 38 | return [profile for _, profile in profiles] 39 | 40 | 41 | def parse_resource_name(path: str) -> str: 42 | def uppercamelcase(s: str) -> str: 43 | return s[:1].upper() + s[1:] 44 | 45 | return "".join(uppercamelcase(p) for p in path.removeprefix("#").split(".")) 46 | 47 | 48 | def unwrap_schema_type( 49 | schema: dict, kind: StructureDefinitionKind | None 50 | ) -> Iterable[tuple[str, list[str]]]: 51 | match kind: 52 | case StructureDefinitionKind.COMPLEX | StructureDefinitionKind.RESOURCE: 53 | return [(parse_resource_name(schema["base"]["path"]), [])] 54 | case _: 55 | if "contentReference" in schema: 56 | return [(parse_resource_name(schema["contentReference"]), [])] 57 | else: 58 | return ((t["code"], t.get("targetProfile", [])) for t in schema["type"]) 59 | 60 | 61 | def parse_property_type( 62 | schema: dict, kind: StructureDefinitionKind | None 63 | ) -> list[StructurePropertyType]: 64 | return [ 65 | StructurePropertyType( 66 | code=parse_type_identifier(type_), 67 | target_profile=parse_target_profile(target_profile), 68 | required=schema["min"] != 0, 69 | isarray=schema["max"] != "1", 70 | ) 71 | for type_, target_profile in unwrap_schema_type(schema, kind) 72 | ] 73 | 74 | 75 | def parse_property_key(schema: dict) -> str: 76 | property_key: str = schema["id"].split(".")[-1] 77 | # 'Choice of Types' are handled by property type, will not parse suffix 78 | return property_key.removesuffix("[x]") 79 | 80 | 81 | def parse_property_kind(schema: dict) -> StructureDefinitionKind | None: 82 | match schema.get("type"): 83 | case [{"code": "BackboneElement"}] | [{"code": "Element"}]: 84 | return StructureDefinitionKind.COMPLEX 85 | case _: 86 | return None 87 | 88 | 89 | def parse_base_structure_definition(definition: dict[str, Any]) -> StructureDefinition: 90 | kind = StructureDefinitionKind.from_str(definition["kind"]) 91 | schemas = definition["snapshot"]["element"] 92 | base_schema = next(s for s in schemas if s["id"] == definition["type"]) 93 | 94 | match kind: 95 | case StructureDefinitionKind.PRIMITIVE: 96 | schemas = definition["differential"]["element"] 97 | structure_schema = next( 98 | s for s in schemas if s["id"] == definition["type"] + ".value" 99 | ) 100 | case _: 101 | structure_schema = base_schema 102 | 103 | match kind: 104 | case StructureDefinitionKind.RESOURCE: 105 | default_elements = { 106 | "resourceType": StructureDefinition( 107 | id=definition["type"], 108 | docstring=base_schema["short"], 109 | type=[ 110 | StructurePropertyType( 111 | code=definition["type"], required=True, literal=True 112 | ) 113 | ], 114 | elements={}, 115 | ) 116 | } 117 | case _: 118 | default_elements = {} 119 | 120 | return StructureDefinition( 121 | id=definition["id"], 122 | kind=kind, 123 | docstring=base_schema["definition"], 124 | type=parse_property_type(structure_schema, kind), 125 | elements=default_elements, 126 | ) 127 | 128 | 129 | def parse_structure_definition(definition: dict[str, Any]) -> StructureDefinition: 130 | structure_definition = parse_base_structure_definition(definition) 131 | schemas = ( 132 | e for e in definition["snapshot"]["element"] if e["id"] != definition["type"] 133 | ) 134 | 135 | for schema in sorted(schemas, key=lambda s: len(s["path"])): 136 | subtree = structure_definition 137 | for path_component in schema["path"].split(".")[1:-1]: 138 | subtree = subtree.elements[path_component] 139 | 140 | property_key = parse_property_key(schema) 141 | property_kind = parse_property_kind(schema) 142 | 143 | subtree.elements[property_key] = StructureDefinition( 144 | id=parse_resource_name(schema["id"]), 145 | docstring=schema["definition"], 146 | type=parse_property_type(schema, property_kind), 147 | kind=property_kind, 148 | elements={}, 149 | ) 150 | 151 | return structure_definition 152 | 153 | 154 | def select_structure_definition_resources( 155 | bundle: DefinitionsBundle, 156 | ) -> Iterable[dict[str, Any]]: 157 | return ( 158 | e["resource"] 159 | for e in bundle["entry"] 160 | if "resource" in e and e["resource"]["resourceType"] == "StructureDefinition" 161 | ) 162 | 163 | 164 | def read_structure_definitions( 165 | bundle: DefinitionsBundle, 166 | ) -> Iterable[StructureDefinition]: 167 | raw_definitions = select_structure_definition_resources(bundle) 168 | 169 | return (parse_structure_definition(definition) for definition in raw_definitions) 170 | 171 | 172 | def load_from_bundle(path: str) -> Iterable[StructureDefinition]: 173 | with open(os.path.abspath(path), encoding="utf8") as schema_file: 174 | return read_structure_definitions(json.load(schema_file)) 175 | -------------------------------------------------------------------------------- /examples/fhirpy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "b1570ff0-3a5d-47ea-a1f7-6c9a4f752424", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stdout", 11 | "output_type": "stream", 12 | "text": [] 13 | } 14 | ], 15 | "source": [ 16 | "%pip install fhirpy mypy pydantic -U" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "id": "f6fa37cc-1619-4980-bbf2-bb9c692244a6", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "from fhirpy import SyncFHIRClient\n", 27 | "\n", 28 | "from generated.resources import Patient, HumanName\n", 29 | "\n", 30 | "FHIR_SERVER_API = \"\"\n", 31 | "FHIR_SERVER_AUTH = \"\"\n", 32 | "\n", 33 | "client = SyncFHIRClient(FHIR_SERVER_API, authorization=FHIR_SERVER_AUTH)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "d7fdbc9b-a143-4a68-84b0-6dcc4c7a34b7", 39 | "metadata": { 40 | "tags": [] 41 | }, 42 | "source": [ 43 | "#### Define helper function to run mypy typechecking on jupyter cells" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 3, 49 | "id": "17c79176-2894-4468-9da3-2d45d22ebdef", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "from mypy import api as typechecker\n", 54 | "\n", 55 | "def typecheck(block: str):\n", 56 | " mypy_error, _, _ = typechecker.run([\n", 57 | " '--no-color-output',\n", 58 | " '--no-error-summary',\n", 59 | " '--ignore-missing-imports',\n", 60 | " '-c', \"\\n\\n\".join([In[2], block])\n", 61 | " ])\n", 62 | " return mypy_error or 'OK'" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "id": "98a28fa7-f292-446e-b979-af8d6a8381b1", 68 | "metadata": {}, 69 | "source": [ 70 | "#### Describe `HumanName` resource with the generated resource model" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 4, 76 | "id": "b893fb05-d230-4f4b-9415-68051e9cb7fb", 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "data": { 81 | "text/plain": [ 82 | "[]" 83 | ] 84 | }, 85 | "execution_count": 4, 86 | "metadata": {}, 87 | "output_type": "execute_result" 88 | } 89 | ], 90 | "source": [ 91 | "name = HumanName(family=\"Smith\", given=[\"Andrew\"])\n", 92 | "\n", 93 | "client.resources(\"Patient\").search(name=name.family).fetch()" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "id": "1a9e778d-89a5-4397-b8c9-a81cb9f8c6ae", 99 | "metadata": {}, 100 | "source": [ 101 | "#### Make sure the resource structure is aligned to the definition" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 5, 107 | "id": "27f1c6eb-7e45-4dea-9a7d-8b0efcbb8fb4", 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "data": { 112 | "text/plain": [ 113 | "'OK'" 114 | ] 115 | }, 116 | "execution_count": 5, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "typecheck(In[4])" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "a245185f-43dc-4d5c-ad1d-8549b5f9b48f", 128 | "metadata": {}, 129 | "source": [ 130 | "#### Should there be any errors in the constructor arguments both resource validation and typecheck would fail" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 6, 136 | "id": "5bdb69e6-91e8-479c-a311-a2db7a5c888d", 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "ename": "ValidationError", 141 | "evalue": "1 validation error for Patient\nname\n value is not a valid list (type=type_error.list)", 142 | "output_type": "error", 143 | "traceback": [ 144 | "\u001b[0;31mValidationError\u001b[0m: 1 validation error for Patient\nname\n value is not a valid list (type=type_error.list)" 145 | ] 146 | } 147 | ], 148 | "source": [ 149 | "patient = Patient(name=HumanName(family=\"Smith\", given=[\"Andrew\"]), gender=\"male\")" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 7, 155 | "id": "2db68eb5-9668-423e-9919-4b1dc13985db", 156 | "metadata": {}, 157 | "outputs": [ 158 | { 159 | "data": { 160 | "text/plain": [ 161 | "':10: error: Argument \"name\" to \"Patient\" has incompatible type \"HumanName\"; expected \"Optional[List[HumanName]]\" [arg-type]\\n'" 162 | ] 163 | }, 164 | "execution_count": 7, 165 | "metadata": {}, 166 | "output_type": "execute_result" 167 | } 168 | ], 169 | "source": [ 170 | "typecheck(In[6])" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "id": "073f57e5-09db-4122-8488-c56d73278052", 176 | "metadata": {}, 177 | "source": [ 178 | "#### Create fhir-py resource from the model" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 8, 184 | "id": "544af06f-b673-4341-b156-0f6678ed310e", 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "client.resource(\"Patient\", **patient.dict(exclude_none=True)).save()" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "id": "6d26ca72-70e5-4b4a-b00a-ff2ccfa03eaf", 194 | "metadata": {}, 195 | "source": [ 196 | "#### Type-cast fhir-py resource back to the definition to work with data fields" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 9, 202 | "id": "63d671bd-b1bc-43c9-80c2-54e0da3d2a36", 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "text/plain": [ 208 | "[HumanName(id=None, use=None, text=None, given=['Andrew'], family='Smith', prefix=None, suffix=None, period=None, extension=None)]" 209 | ] 210 | }, 211 | "execution_count": 9, 212 | "metadata": {}, 213 | "output_type": "execute_result" 214 | } 215 | ], 216 | "source": [ 217 | "patient = Patient.parse_obj(client.resources(\"Patient\").search(name=\"Smith\").first())\n", 218 | "patient.name" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 10, 224 | "id": "accdc45a-f1b4-4289-955c-627bb554d745", 225 | "metadata": {}, 226 | "outputs": [ 227 | { 228 | "data": { 229 | "text/plain": [ 230 | "'OK'" 231 | ] 232 | }, 233 | "execution_count": 10, 234 | "metadata": {}, 235 | "output_type": "execute_result" 236 | } 237 | ], 238 | "source": [ 239 | "typecheck(In[9])" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "id": "aaa8dcee-0242-412a-aa73-cd6d88e0c291", 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [] 249 | } 250 | ], 251 | "metadata": { 252 | "kernelspec": { 253 | "display_name": "Python 3 (ipykernel)", 254 | "language": "python", 255 | "name": "python3" 256 | }, 257 | "language_info": { 258 | "codemirror_mode": { 259 | "name": "ipython", 260 | "version": 3 261 | }, 262 | "file_extension": ".py", 263 | "mimetype": "text/x-python", 264 | "name": "python", 265 | "nbconvert_exporter": "python", 266 | "pygments_lexer": "ipython3", 267 | "version": "3.10.8" 268 | } 269 | }, 270 | "nbformat": 4, 271 | "nbformat_minor": 5 272 | } 273 | -------------------------------------------------------------------------------- /fhir_py_types/ast.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import itertools 3 | import keyword 4 | import logging 5 | from collections.abc import Iterable 6 | from dataclasses import replace 7 | from enum import Enum, auto 8 | from typing import Literal 9 | 10 | from fhir_py_types import ( 11 | StructureDefinition, 12 | StructureDefinitionKind, 13 | StructurePropertyType, 14 | is_polymorphic, 15 | is_primitive_type, 16 | ) 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class AnnotationForm(Enum): 22 | Property = auto() 23 | TypeAlias = auto() 24 | Dict = auto() 25 | 26 | 27 | def make_type_annotation( 28 | type_: StructurePropertyType, form: AnnotationForm 29 | ) -> ast.expr: 30 | match form: 31 | case AnnotationForm.TypeAlias: 32 | annotation: ast.expr = ast.Name(type_.code) 33 | case _: 34 | annotation = ast.Constant(type_.code) 35 | 36 | if type_.literal: 37 | annotation = ast.Subscript(value=ast.Name("Literal_"), slice=annotation) 38 | 39 | if type_.isarray: 40 | annotation = ast.Subscript(value=ast.Name("List_"), slice=annotation) 41 | 42 | if not type_.required and form != AnnotationForm.TypeAlias: 43 | annotation = ast.Subscript(value=ast.Name("Optional_"), slice=annotation) 44 | 45 | return annotation 46 | 47 | 48 | def make_default_initializer( 49 | identifier: str, type_: StructurePropertyType 50 | ) -> ast.expr | None: 51 | default: ast.expr | None = None 52 | 53 | if keyword.iskeyword(identifier) or type_.alias: 54 | default = ast.Call( 55 | ast.Name("Field"), 56 | args=[], 57 | keywords=[ 58 | *( 59 | [ast.keyword(arg="default", value=ast.Constant(None))] 60 | if not type_.required 61 | else [] 62 | ), 63 | ast.keyword(arg="alias", value=ast.Constant(type_.alias or identifier)), 64 | ], 65 | ) 66 | else: 67 | if not type_.required: 68 | default = ast.Constant(None) 69 | elif type_.literal and not type_.isarray: 70 | default = ast.Constant(type_.code) 71 | 72 | return default 73 | 74 | 75 | def format_identifier( 76 | definition: StructureDefinition, identifier: str, type_: StructurePropertyType 77 | ) -> str: 78 | def uppercamelcase(s: str) -> str: 79 | return s[:1].upper() + s[1:] 80 | 81 | if is_polymorphic(definition): 82 | # TODO(Vadim): it's fast hack # noqa: TD003 83 | if type_.code[0].islower(): 84 | return identifier + uppercamelcase(clear_primitive_id(type_.code)) 85 | return identifier + uppercamelcase(type_.code) 86 | 87 | return identifier 88 | 89 | 90 | def remap_type( 91 | definition: StructureDefinition, type_: StructurePropertyType 92 | ) -> StructurePropertyType: 93 | if not type_.literal: 94 | match type_.code: 95 | case "Resource": 96 | # Different contexts use 'Resource' type to refer to any 97 | # resource differentiated by its 'resourceType' (tagged union). 98 | # 'AnyResource' is defined in header as a special type 99 | # that dynamically replaced with a right type in run-time 100 | type_ = replace(type_, code="AnyResource") 101 | 102 | if is_polymorphic(definition): 103 | # Required polymorphic types are not yet supported. 104 | # Making multiple polymorphic properties required means 105 | # no valid resource model can be generated (due to required conflicts). 106 | # Future implementation might include optional properties 107 | # with a custom validator that will enforce single required property rule. 108 | type_ = replace(type_, required=False) 109 | 110 | if is_primitive_type(type_): 111 | # Primitive types defined from the small letter (like code) 112 | # and it might overlap with model fields 113 | # e.g. QuestionnaireItem has attribute code and linkId has type code 114 | type_ = replace(type_, code=make_primitive_id(type_.code)) 115 | 116 | return type_ 117 | 118 | 119 | def zip_identifier_type( 120 | definition: StructureDefinition, identifier: str 121 | ) -> Iterable[tuple[str, StructurePropertyType]]: 122 | result = [] 123 | 124 | for t in [remap_type(definition, t) for t in definition.type]: 125 | name = format_identifier(definition, identifier, t) 126 | result.append((name, t)) 127 | if definition.kind != StructureDefinitionKind.PRIMITIVE and is_primitive_type( 128 | t 129 | ): 130 | result.append( 131 | ( 132 | f"{name}__ext", 133 | StructurePropertyType( 134 | code="Element", 135 | target_profile=[], 136 | required=False, 137 | isarray=definition.type[0].isarray, 138 | alias=f"_{name}" 139 | ), 140 | ) 141 | ) 142 | 143 | return result 144 | 145 | 146 | def make_assignment_statement( 147 | target: str, 148 | annotation: ast.expr, 149 | form: Literal[AnnotationForm.Property, AnnotationForm.TypeAlias], 150 | default: ast.expr | None = None, 151 | ) -> ast.stmt: 152 | match form: 153 | case AnnotationForm.Property: 154 | return ast.AnnAssign( 155 | target=ast.Name(target), annotation=annotation, simple=1, value=default 156 | ) 157 | case AnnotationForm.TypeAlias: 158 | return ast.Assign(targets=[ast.Name(target)], value=annotation) 159 | 160 | 161 | def type_annotate( 162 | definition: StructureDefinition, 163 | identifier: str, 164 | form: Literal[AnnotationForm.Property, AnnotationForm.TypeAlias], 165 | ) -> Iterable[ast.stmt]: 166 | return itertools.chain.from_iterable( 167 | [ 168 | make_assignment_statement( 169 | identifier_ + "_" if keyword.iskeyword(identifier_) else identifier_, 170 | make_type_annotation(type_, form), 171 | form, 172 | default=make_default_initializer(identifier_, type_), 173 | ), 174 | ast.Expr(value=ast.Constant(definition.docstring)), 175 | ] 176 | for (identifier_, type_) in zip_identifier_type(definition, identifier) 177 | ) 178 | 179 | 180 | def order_type_overriding_properties( 181 | properties_definition: dict[str, StructureDefinition], 182 | ) -> Iterable[tuple[str, StructureDefinition]]: 183 | property_types = { 184 | t.code for definition in properties_definition.values() for t in definition.type 185 | } 186 | return sorted( 187 | properties_definition.items(), 188 | key=lambda definition: 1 if definition[0] in property_types else -1, 189 | ) 190 | 191 | 192 | def define_class_object( 193 | definition: StructureDefinition, 194 | ) -> Iterable[ast.stmt | ast.expr]: 195 | bases: list[ast.expr] = [] 196 | if definition.kind == StructureDefinitionKind.RESOURCE: 197 | bases.append(ast.Name("AnyResource")) 198 | # BaseModel should be the last, because it overrides `extra` 199 | bases.append(ast.Name("BaseModel")) 200 | 201 | return [ 202 | ast.ClassDef( 203 | definition.id, 204 | bases=bases, 205 | body=[ 206 | ast.Expr(value=ast.Constant(definition.docstring)), 207 | *itertools.chain.from_iterable( 208 | type_annotate(property, identifier, AnnotationForm.Property) 209 | for identifier, property in order_type_overriding_properties( 210 | definition.elements 211 | ) 212 | ), 213 | ], 214 | decorator_list=[], 215 | keywords=[], 216 | type_params=[], 217 | ), 218 | ] 219 | 220 | 221 | def define_class(definition: StructureDefinition) -> Iterable[ast.stmt | ast.expr]: 222 | return define_class_object(definition) 223 | 224 | 225 | def define_alias(definition: StructureDefinition) -> Iterable[ast.stmt]: 226 | # Primitive types are renamed to another name to avoid overlapping with model fields 227 | return type_annotate( 228 | definition, make_primitive_id(definition.id), AnnotationForm.TypeAlias 229 | ) 230 | 231 | 232 | def make_primitive_id(name: str) -> str: 233 | if name in ("str", "int", "float", "bool"): 234 | return name 235 | return f"{name}Type" 236 | 237 | 238 | def clear_primitive_id(name: str) -> str: 239 | if name.endswith("Type"): 240 | return name[:-4] 241 | return name 242 | 243 | 244 | def select_nested_definitions( 245 | definition: StructureDefinition, 246 | ) -> Iterable[StructureDefinition]: 247 | return ( 248 | d 249 | for d in definition.elements.values() 250 | if d.kind == StructureDefinitionKind.COMPLEX 251 | ) 252 | 253 | 254 | def iterate_definitions_tree( 255 | root: StructureDefinition, 256 | ) -> Iterable[StructureDefinition]: 257 | subtree = list(select_nested_definitions(root)) 258 | 259 | while subtree: 260 | tree_node = subtree.pop() 261 | yield tree_node 262 | subtree.extend(select_nested_definitions(tree_node)) 263 | 264 | yield root 265 | 266 | 267 | def build_ast( 268 | structure_definitions: Iterable[StructureDefinition], 269 | ) -> Iterable[ast.stmt | ast.expr]: 270 | structure_definitions = list(structure_definitions) 271 | typedefinitions: list[ast.stmt | ast.expr] = [] 272 | 273 | for root in structure_definitions: 274 | for definition in iterate_definitions_tree(root): 275 | match definition.kind: 276 | case StructureDefinitionKind.RESOURCE | StructureDefinitionKind.COMPLEX: 277 | typedefinitions.extend(define_class(definition)) 278 | 279 | case StructureDefinitionKind.PRIMITIVE: 280 | typedefinitions.extend(define_alias(definition)) 281 | 282 | case _: 283 | logger.warning( 284 | f"Unsupported definition {definition.id} of kind {definition.kind}, skipping" 285 | ) 286 | 287 | return sorted( 288 | typedefinitions, 289 | # Defer any postprocessing until after the structure tree is defined. 290 | key=lambda definition: 1 if isinstance(definition, ast.Call) else 0, 291 | ) 292 | -------------------------------------------------------------------------------- /tests/test_ast.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Sequence 3 | 4 | import pytest 5 | 6 | from fhir_py_types import ( 7 | StructureDefinition, 8 | StructureDefinitionKind, 9 | StructurePropertyType, 10 | ) 11 | from fhir_py_types.ast import build_ast 12 | 13 | 14 | def assert_eq( 15 | definitions: Sequence[StructureDefinition], ast_tree: Sequence[ast.stmt | ast.expr] 16 | ) -> None: 17 | generated = [ast.dump(t) for t in build_ast(definitions)] 18 | expected = [ast.dump(t) for t in ast_tree] 19 | 20 | assert generated == expected 21 | 22 | 23 | def build_field_with_alias(identifier: str) -> ast.Call: 24 | return ast.Call( 25 | func=ast.Name(id="Field"), 26 | args=[], 27 | keywords=[ 28 | ast.keyword(arg="default", value=ast.Constant(value=None)), 29 | ast.keyword(arg="alias", value=ast.Constant(value=identifier)), 30 | ], 31 | ) 32 | 33 | 34 | def test_generates_empty_ast_from_empty_definitions() -> None: 35 | assert build_ast([]) == [] 36 | 37 | 38 | def test_generates_class_for_flat_definition() -> None: 39 | assert_eq( 40 | [ 41 | StructureDefinition( 42 | id="TestResource", 43 | docstring="test resource description", 44 | type=[ 45 | StructurePropertyType( 46 | code="TestResource", required=True, isarray=False 47 | ) 48 | ], 49 | elements={ 50 | "property1": StructureDefinition( 51 | id="property1", 52 | docstring="test resource property 1", 53 | type=[ 54 | StructurePropertyType( 55 | code="str", required=True, isarray=False 56 | ) 57 | ], 58 | elements={}, 59 | ) 60 | }, 61 | kind=StructureDefinitionKind.RESOURCE, 62 | ) 63 | ], 64 | [ 65 | ast.ClassDef( 66 | name="TestResource", 67 | bases=[ast.Name(id='AnyResource'), ast.Name(id="BaseModel")], 68 | keywords=[], 69 | body=[ 70 | ast.Expr(value=ast.Constant(value="test resource description")), 71 | ast.AnnAssign( 72 | target=ast.Name(id="property1"), 73 | annotation=ast.Constant("str"), 74 | simple=1, 75 | ), 76 | ast.Expr(value=ast.Constant(value="test resource property 1")), 77 | ast.AnnAssign( 78 | target=ast.Name(id="property1__ext"), 79 | annotation=ast.Subscript( 80 | value=ast.Name(id="Optional_"), 81 | slice=ast.Constant("Element"), 82 | ), 83 | value=build_field_with_alias("_property1"), 84 | simple=1, 85 | ), 86 | ast.Expr(value=ast.Constant(value="test resource property 1")), 87 | ], 88 | decorator_list=[], 89 | type_params=[], 90 | ), 91 | ], 92 | ) 93 | 94 | 95 | @pytest.mark.parametrize( 96 | ("definitions", "ast_tree"), 97 | [ 98 | ( 99 | [ 100 | StructureDefinition( 101 | id="date", 102 | docstring="date description", 103 | type=[ 104 | StructurePropertyType(code="str", required=True, isarray=False) 105 | ], 106 | elements={}, 107 | kind=StructureDefinitionKind.PRIMITIVE, 108 | ) 109 | ], 110 | [ 111 | ast.Assign( 112 | targets=[ast.Name("dateType")], 113 | value=ast.Name("str"), 114 | ), 115 | ast.Expr(value=ast.Constant("date description")), 116 | ], 117 | ), 118 | ], 119 | ) 120 | def test_generates_alias_for_primitive_kind_definition( 121 | definitions: list[StructureDefinition], ast_tree: list[ast.stmt] 122 | ) -> None: 123 | assert_eq(definitions, ast_tree) 124 | 125 | 126 | def test_generates_multiple_classes_for_compound_definition() -> None: 127 | assert_eq( 128 | [ 129 | StructureDefinition( 130 | id="TestResource", 131 | docstring="test resource description", 132 | type=[ 133 | StructurePropertyType( 134 | code="TestResource", required=True, isarray=False 135 | ) 136 | ], 137 | elements={ 138 | "complexproperty": StructureDefinition( 139 | id="NestedComplex", 140 | docstring="nested complex definition", 141 | type=[ 142 | StructurePropertyType( 143 | code="NestedTestResource", required=True, isarray=False 144 | ) 145 | ], 146 | elements={ 147 | "property1": StructureDefinition( 148 | id="property1", 149 | docstring="nested test resource property 1", 150 | type=[ 151 | StructurePropertyType( 152 | code="str", required=False, isarray=False 153 | ) 154 | ], 155 | elements={}, 156 | ) 157 | }, 158 | kind=StructureDefinitionKind.COMPLEX, 159 | ) 160 | }, 161 | kind=StructureDefinitionKind.RESOURCE, 162 | ) 163 | ], 164 | [ 165 | ast.ClassDef( 166 | name="NestedComplex", 167 | bases=[ast.Name(id="BaseModel")], 168 | keywords=[], 169 | body=[ 170 | ast.Expr(value=ast.Constant(value="nested complex definition")), 171 | ast.AnnAssign( 172 | target=ast.Name(id="property1"), 173 | annotation=ast.Subscript( 174 | value=ast.Name(id="Optional_"), slice=ast.Constant("str") 175 | ), 176 | simple=1, 177 | value=ast.Constant(None), 178 | ), 179 | ast.Expr( 180 | value=ast.Constant(value="nested test resource property 1") 181 | ), 182 | ast.AnnAssign( 183 | target=ast.Name(id="property1__ext"), 184 | annotation=ast.Subscript( 185 | value=ast.Name(id="Optional_"), 186 | slice=ast.Constant("Element"), 187 | ), 188 | simple=1, 189 | value=build_field_with_alias("_property1"), 190 | ), 191 | ast.Expr( 192 | value=ast.Constant(value="nested test resource property 1") 193 | ), 194 | ], 195 | decorator_list=[], 196 | type_params=[], 197 | ), 198 | ast.ClassDef( 199 | name="TestResource", 200 | bases=[ast.Name(id="AnyResource"), ast.Name(id="BaseModel")], 201 | keywords=[], 202 | body=[ 203 | ast.Expr(value=ast.Constant(value="test resource description")), 204 | ast.AnnAssign( 205 | target=ast.Name(id="complexproperty"), 206 | annotation=ast.Constant("NestedTestResource"), 207 | simple=1, 208 | ), 209 | ast.Expr(value=ast.Constant(value="nested complex definition")), 210 | ], 211 | decorator_list=[], 212 | type_params=[], 213 | ), 214 | ], 215 | ) 216 | 217 | 218 | @pytest.mark.parametrize( 219 | ("required", "isarray", "literal", "expected_annotation"), 220 | [ 221 | ( 222 | False, 223 | False, 224 | False, 225 | ast.Subscript(value=ast.Name(id="Optional_"), slice=ast.Constant("str")), 226 | ), 227 | ( 228 | True, 229 | False, 230 | False, 231 | ast.Constant("str"), 232 | ), 233 | ( 234 | False, 235 | True, 236 | False, 237 | ast.Subscript( 238 | value=ast.Name(id="Optional_"), 239 | slice=ast.Subscript( 240 | value=ast.Name(id="List_"), slice=ast.Constant("str") 241 | ), 242 | ), 243 | ), 244 | ( 245 | True, 246 | True, 247 | False, 248 | ast.Subscript(value=ast.Name(id="List_"), slice=ast.Constant("str")), 249 | ), 250 | ( 251 | True, 252 | False, 253 | True, 254 | ast.Subscript(value=ast.Name(id="Literal_"), slice=ast.Constant("str")), 255 | ), 256 | ( 257 | False, 258 | False, 259 | True, 260 | ast.Subscript( 261 | value=ast.Name(id="Optional_"), 262 | slice=ast.Subscript( 263 | value=ast.Name(id="Literal_"), slice=ast.Constant("str") 264 | ), 265 | ), 266 | ), 267 | ], 268 | ) 269 | def test_generates_annotations_according_to_structure_type( 270 | required: bool, 271 | isarray: bool, 272 | literal: bool, 273 | expected_annotation: ast.Subscript | ast.Constant, 274 | ) -> None: 275 | assert_eq( 276 | [ 277 | StructureDefinition( 278 | id="TestResource", 279 | docstring="test resource description", 280 | type=[ 281 | StructurePropertyType( 282 | code="TestResource", required=True, isarray=False 283 | ) 284 | ], 285 | elements={ 286 | "property1": StructureDefinition( 287 | id="property1", 288 | docstring="test resource property 1", 289 | type=[ 290 | StructurePropertyType( 291 | code="str", 292 | required=required, 293 | isarray=isarray, 294 | literal=literal, 295 | ) 296 | ], 297 | elements={}, 298 | ) 299 | }, 300 | kind=StructureDefinitionKind.RESOURCE, 301 | ) 302 | ], 303 | [ 304 | ast.ClassDef( 305 | name="TestResource", 306 | bases=[ast.Name(id='AnyResource'), ast.Name(id="BaseModel")], 307 | keywords=[], 308 | body=[ 309 | ast.Expr(value=ast.Constant(value="test resource description")), 310 | ast.AnnAssign( 311 | target=ast.Name(id="property1"), 312 | annotation=expected_annotation, 313 | simple=1, 314 | value=ast.Constant(None) 315 | if not required 316 | else ast.Constant("str") 317 | if literal 318 | else None, 319 | ), 320 | ast.Expr(value=ast.Constant(value="test resource property 1")), 321 | ast.AnnAssign( 322 | target=ast.Name(id="property1__ext"), 323 | annotation=ast.Subscript( 324 | value=ast.Name(id="Optional_"), 325 | slice=ast.Subscript( 326 | value=ast.Name(id="List_"), 327 | slice=ast.Constant("Element"), 328 | ), 329 | ) 330 | if isarray 331 | else ast.Subscript( 332 | value=ast.Name(id="Optional_"), 333 | slice=ast.Constant("Element"), 334 | ), 335 | simple=1, 336 | value=build_field_with_alias("_property1"), 337 | ), 338 | ast.Expr(value=ast.Constant(value="test resource property 1")), 339 | ], 340 | decorator_list=[], 341 | type_params=[], 342 | ), 343 | ], 344 | ) 345 | 346 | 347 | def test_unrolls_required_polymorphic_into_class_union() -> None: 348 | assert_eq( 349 | [ 350 | StructureDefinition( 351 | id="TestResource", 352 | docstring="test resource description", 353 | type=[ 354 | StructurePropertyType( 355 | code="TestResource", required=True, isarray=False 356 | ) 357 | ], 358 | elements={ 359 | "monotype": StructureDefinition( 360 | id="monotype", 361 | docstring="monotype property definition", 362 | type=[ 363 | StructurePropertyType( 364 | code="boolean", required=False, isarray=False 365 | ), 366 | ], 367 | elements={}, 368 | ), 369 | "value": StructureDefinition( 370 | id="value", 371 | docstring="polymorphic property definition", 372 | type=[ 373 | StructurePropertyType( 374 | code="boolean", required=True, isarray=False 375 | ), 376 | StructurePropertyType( 377 | code="Quantity", required=True, isarray=False 378 | ), 379 | ], 380 | elements={}, 381 | ), 382 | }, 383 | kind=StructureDefinitionKind.RESOURCE, 384 | ) 385 | ], 386 | [ 387 | ast.ClassDef( 388 | name="TestResource", 389 | bases=[ast.Name(id='AnyResource'),ast.Name(id="BaseModel")], 390 | keywords=[], 391 | body=[ 392 | ast.Expr(value=ast.Constant(value="test resource description")), 393 | ast.AnnAssign( 394 | target=ast.Name(id="monotype"), 395 | annotation=ast.Subscript( 396 | value=ast.Name(id="Optional_"), 397 | slice=ast.Constant("booleanType"), 398 | ), 399 | simple=1, 400 | value=ast.Constant(None), 401 | ), 402 | ast.Expr(value=ast.Constant(value="monotype property definition")), 403 | ast.AnnAssign( 404 | target=ast.Name(id="monotype__ext"), 405 | annotation=ast.Subscript( 406 | value=ast.Name(id="Optional_"), 407 | slice=ast.Constant("Element"), 408 | ), 409 | simple=1, 410 | value=build_field_with_alias("_monotype"), 411 | ), 412 | ast.Expr(value=ast.Constant(value="monotype property definition")), 413 | ast.AnnAssign( 414 | target=ast.Name(id="valueBoolean"), 415 | annotation=ast.Subscript( 416 | value=ast.Name(id="Optional_"), 417 | slice=ast.Constant("booleanType"), 418 | ), 419 | simple=1, 420 | value=ast.Constant(None), 421 | ), 422 | ast.Expr( 423 | value=ast.Constant(value="polymorphic property definition") 424 | ), 425 | ast.AnnAssign( 426 | target=ast.Name(id="valueBoolean__ext"), 427 | annotation=ast.Subscript( 428 | value=ast.Name(id="Optional_"), 429 | slice=ast.Constant("Element"), 430 | ), 431 | simple=1, 432 | value=build_field_with_alias("_valueBoolean"), 433 | ), 434 | ast.Expr( 435 | value=ast.Constant(value="polymorphic property definition") 436 | ), 437 | ast.AnnAssign( 438 | target=ast.Name(id="valueQuantity"), 439 | annotation=ast.Subscript( 440 | value=ast.Name(id="Optional_"), 441 | slice=ast.Constant("Quantity"), 442 | ), 443 | simple=1, 444 | value=ast.Constant(None), 445 | ), 446 | ast.Expr( 447 | value=ast.Constant(value="polymorphic property definition") 448 | ), 449 | ], 450 | decorator_list=[], 451 | type_params=[], 452 | ), 453 | ], 454 | ) 455 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.4.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 22 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | 29 | [package.extras] 30 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 31 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 32 | trio = ["trio (>=0.23)"] 33 | 34 | [[package]] 35 | name = "autohooks" 36 | version = "23.10.0" 37 | description = "Library for managing git hooks" 38 | optional = false 39 | python-versions = ">=3.9,<4.0" 40 | files = [ 41 | {file = "autohooks-23.10.0-py3-none-any.whl", hash = "sha256:a4fdf33097daae47068c1ce6ec075a1d5fc692a0a182b0c69e8abda75b45c39e"}, 42 | {file = "autohooks-23.10.0.tar.gz", hash = "sha256:4de41476ed72e0a3670c40b1fc9b7d67c66eb788ab01b6a7b516fcebb2283166"}, 43 | ] 44 | 45 | [package.dependencies] 46 | pontos = ">=22.8.0" 47 | rich = ">=12.5.1" 48 | tomlkit = ">=0.5.11" 49 | 50 | [[package]] 51 | name = "autohooks-plugin-black" 52 | version = "23.10.0" 53 | description = "An autohooks plugin for python code formatting via black" 54 | optional = false 55 | python-versions = ">=3.9,<4.0" 56 | files = [ 57 | {file = "autohooks_plugin_black-23.10.0-py3-none-any.whl", hash = "sha256:88d648251df749586af9ea5be3105daa4358ed916b61aee738d0727387214470"}, 58 | {file = "autohooks_plugin_black-23.10.0.tar.gz", hash = "sha256:8415b5f566d861236bde2b0973699f64a8b861208af4fa05fe04a1f923ea3ef6"}, 59 | ] 60 | 61 | [package.dependencies] 62 | autohooks = ">=21.6.0" 63 | black = ">=20.8" 64 | 65 | [[package]] 66 | name = "autohooks-plugin-mypy" 67 | version = "23.10.0" 68 | description = "An autohooks plugin for python code static typing check with mypy" 69 | optional = false 70 | python-versions = ">=3.9,<4.0" 71 | files = [ 72 | {file = "autohooks_plugin_mypy-23.10.0-py3-none-any.whl", hash = "sha256:8ac36b74900b2f2456fec046126e564374acd6de2752d87255c6f71c4e6a73ff"}, 73 | {file = "autohooks_plugin_mypy-23.10.0.tar.gz", hash = "sha256:ebefaa83074b662de38c914f6cac9f4f8e3452e36f54a5834df3f1590cc0c540"}, 74 | ] 75 | 76 | [package.dependencies] 77 | autohooks = ">=21.7.0" 78 | mypy = ">=0.910" 79 | 80 | [[package]] 81 | name = "autohooks-plugin-ruff" 82 | version = "23.11.0" 83 | description = "An autohooks plugin for python code formatting via ruff" 84 | optional = false 85 | python-versions = ">=3.9,<4.0" 86 | files = [ 87 | {file = "autohooks_plugin_ruff-23.11.0-py3-none-any.whl", hash = "sha256:26f7be03e232a505e65246b666ea48a323a900d05bdef68e337a5c93dfdb23b1"}, 88 | {file = "autohooks_plugin_ruff-23.11.0.tar.gz", hash = "sha256:1f6c3bc92449fc57c71dd4ac0a410d7f5229f3aff96921fa80c70a28d7db9366"}, 89 | ] 90 | 91 | [package.dependencies] 92 | autohooks = ">=23.4.0" 93 | ruff = ">=0.0.272" 94 | 95 | [[package]] 96 | name = "black" 97 | version = "23.12.1" 98 | description = "The uncompromising code formatter." 99 | optional = false 100 | python-versions = ">=3.8" 101 | files = [ 102 | {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, 103 | {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, 104 | {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, 105 | {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, 106 | {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, 107 | {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, 108 | {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, 109 | {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, 110 | {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, 111 | {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, 112 | {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, 113 | {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, 114 | {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, 115 | {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, 116 | {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, 117 | {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, 118 | {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, 119 | {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, 120 | {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, 121 | {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, 122 | {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, 123 | {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, 124 | ] 125 | 126 | [package.dependencies] 127 | click = ">=8.0.0" 128 | mypy-extensions = ">=0.4.3" 129 | packaging = ">=22.0" 130 | pathspec = ">=0.9.0" 131 | platformdirs = ">=2" 132 | 133 | [package.extras] 134 | colorama = ["colorama (>=0.4.3)"] 135 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 136 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 137 | uvloop = ["uvloop (>=0.15.2)"] 138 | 139 | [[package]] 140 | name = "certifi" 141 | version = "2024.8.30" 142 | description = "Python package for providing Mozilla's CA Bundle." 143 | optional = false 144 | python-versions = ">=3.6" 145 | files = [ 146 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 147 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 148 | ] 149 | 150 | [[package]] 151 | name = "click" 152 | version = "8.1.7" 153 | description = "Composable command line interface toolkit" 154 | optional = false 155 | python-versions = ">=3.7" 156 | files = [ 157 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 158 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 159 | ] 160 | 161 | [package.dependencies] 162 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 163 | 164 | [[package]] 165 | name = "colorama" 166 | version = "0.4.6" 167 | description = "Cross-platform colored terminal text." 168 | optional = false 169 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 170 | files = [ 171 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 172 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 173 | ] 174 | 175 | [[package]] 176 | name = "colorful" 177 | version = "0.5.6" 178 | description = "Terminal string styling done right, in Python." 179 | optional = false 180 | python-versions = "*" 181 | files = [ 182 | {file = "colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e"}, 183 | {file = "colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d"}, 184 | ] 185 | 186 | [package.dependencies] 187 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 188 | 189 | [[package]] 190 | name = "h11" 191 | version = "0.14.0" 192 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 193 | optional = false 194 | python-versions = ">=3.7" 195 | files = [ 196 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 197 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 198 | ] 199 | 200 | [[package]] 201 | name = "h2" 202 | version = "4.1.0" 203 | description = "HTTP/2 State-Machine based protocol implementation" 204 | optional = false 205 | python-versions = ">=3.6.1" 206 | files = [ 207 | {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, 208 | {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, 209 | ] 210 | 211 | [package.dependencies] 212 | hpack = ">=4.0,<5" 213 | hyperframe = ">=6.0,<7" 214 | 215 | [[package]] 216 | name = "hpack" 217 | version = "4.0.0" 218 | description = "Pure-Python HPACK header compression" 219 | optional = false 220 | python-versions = ">=3.6.1" 221 | files = [ 222 | {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, 223 | {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, 224 | ] 225 | 226 | [[package]] 227 | name = "httpcore" 228 | version = "1.0.5" 229 | description = "A minimal low-level HTTP client." 230 | optional = false 231 | python-versions = ">=3.8" 232 | files = [ 233 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 234 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 235 | ] 236 | 237 | [package.dependencies] 238 | certifi = "*" 239 | h11 = ">=0.13,<0.15" 240 | 241 | [package.extras] 242 | asyncio = ["anyio (>=4.0,<5.0)"] 243 | http2 = ["h2 (>=3,<5)"] 244 | socks = ["socksio (==1.*)"] 245 | trio = ["trio (>=0.22.0,<0.26.0)"] 246 | 247 | [[package]] 248 | name = "httpx" 249 | version = "0.27.2" 250 | description = "The next generation HTTP client." 251 | optional = false 252 | python-versions = ">=3.8" 253 | files = [ 254 | {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, 255 | {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, 256 | ] 257 | 258 | [package.dependencies] 259 | anyio = "*" 260 | certifi = "*" 261 | h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} 262 | httpcore = "==1.*" 263 | idna = "*" 264 | sniffio = "*" 265 | 266 | [package.extras] 267 | brotli = ["brotli", "brotlicffi"] 268 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 269 | http2 = ["h2 (>=3,<5)"] 270 | socks = ["socksio (==1.*)"] 271 | zstd = ["zstandard (>=0.18.0)"] 272 | 273 | [[package]] 274 | name = "hyperframe" 275 | version = "6.0.1" 276 | description = "HTTP/2 framing layer for Python" 277 | optional = false 278 | python-versions = ">=3.6.1" 279 | files = [ 280 | {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, 281 | {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, 282 | ] 283 | 284 | [[package]] 285 | name = "idna" 286 | version = "3.8" 287 | description = "Internationalized Domain Names in Applications (IDNA)" 288 | optional = false 289 | python-versions = ">=3.6" 290 | files = [ 291 | {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, 292 | {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, 293 | ] 294 | 295 | [[package]] 296 | name = "iniconfig" 297 | version = "2.0.0" 298 | description = "brain-dead simple config-ini parsing" 299 | optional = false 300 | python-versions = ">=3.7" 301 | files = [ 302 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 303 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 304 | ] 305 | 306 | [[package]] 307 | name = "lxml" 308 | version = "5.3.0" 309 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 310 | optional = false 311 | python-versions = ">=3.6" 312 | files = [ 313 | {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, 314 | {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, 315 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, 316 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, 317 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, 318 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, 319 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, 320 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, 321 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, 322 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, 323 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, 324 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, 325 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, 326 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, 327 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, 328 | {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, 329 | {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, 330 | {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, 331 | {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, 332 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, 333 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, 334 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, 335 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, 336 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, 337 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, 338 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, 339 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, 340 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, 341 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, 342 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, 343 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, 344 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, 345 | {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, 346 | {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, 347 | {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, 348 | {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, 349 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, 350 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, 351 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, 352 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, 353 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, 354 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, 355 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, 356 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, 357 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, 358 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, 359 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, 360 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, 361 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, 362 | {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, 363 | {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, 364 | {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, 365 | {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, 366 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, 367 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, 368 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, 369 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, 370 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, 371 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, 372 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, 373 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, 374 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, 375 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, 376 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, 377 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, 378 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, 379 | {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, 380 | {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, 381 | {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, 382 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, 383 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, 384 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, 385 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, 386 | {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, 387 | {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, 388 | {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, 389 | {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, 390 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, 391 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, 392 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, 393 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, 394 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, 395 | {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, 396 | {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, 397 | {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, 398 | {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, 399 | {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, 400 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, 401 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, 402 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, 403 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, 404 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, 405 | {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, 406 | {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, 407 | {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, 408 | {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, 409 | {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, 410 | {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, 411 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, 412 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, 413 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, 414 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, 415 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, 416 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, 417 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, 418 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, 419 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, 420 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, 421 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, 422 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, 423 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, 424 | {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, 425 | {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, 426 | {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, 427 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, 428 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, 429 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, 430 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, 431 | {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, 432 | {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, 433 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, 434 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, 435 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, 436 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, 437 | {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, 438 | {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, 439 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, 440 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, 441 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, 442 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, 443 | {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, 444 | {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, 445 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, 446 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, 447 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, 448 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, 449 | {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, 450 | {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, 451 | ] 452 | 453 | [package.extras] 454 | cssselect = ["cssselect (>=0.7)"] 455 | html-clean = ["lxml-html-clean"] 456 | html5 = ["html5lib"] 457 | htmlsoup = ["BeautifulSoup4"] 458 | source = ["Cython (>=3.0.11)"] 459 | 460 | [[package]] 461 | name = "markdown-it-py" 462 | version = "3.0.0" 463 | description = "Python port of markdown-it. Markdown parsing, done right!" 464 | optional = false 465 | python-versions = ">=3.8" 466 | files = [ 467 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 468 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 469 | ] 470 | 471 | [package.dependencies] 472 | mdurl = ">=0.1,<1.0" 473 | 474 | [package.extras] 475 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 476 | code-style = ["pre-commit (>=3.0,<4.0)"] 477 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 478 | linkify = ["linkify-it-py (>=1,<3)"] 479 | plugins = ["mdit-py-plugins"] 480 | profiling = ["gprof2dot"] 481 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 482 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 483 | 484 | [[package]] 485 | name = "mdurl" 486 | version = "0.1.2" 487 | description = "Markdown URL utilities" 488 | optional = false 489 | python-versions = ">=3.7" 490 | files = [ 491 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 492 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 493 | ] 494 | 495 | [[package]] 496 | name = "mypy" 497 | version = "1.11.2" 498 | description = "Optional static typing for Python" 499 | optional = false 500 | python-versions = ">=3.8" 501 | files = [ 502 | {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, 503 | {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, 504 | {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, 505 | {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, 506 | {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, 507 | {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, 508 | {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, 509 | {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, 510 | {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, 511 | {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, 512 | {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, 513 | {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, 514 | {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, 515 | {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, 516 | {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, 517 | {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, 518 | {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, 519 | {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, 520 | {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, 521 | {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, 522 | {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, 523 | {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, 524 | {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, 525 | {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, 526 | {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, 527 | {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, 528 | {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, 529 | ] 530 | 531 | [package.dependencies] 532 | mypy-extensions = ">=1.0.0" 533 | typing-extensions = ">=4.6.0" 534 | 535 | [package.extras] 536 | dmypy = ["psutil (>=4.0)"] 537 | install-types = ["pip"] 538 | mypyc = ["setuptools (>=50)"] 539 | reports = ["lxml"] 540 | 541 | [[package]] 542 | name = "mypy-extensions" 543 | version = "1.0.0" 544 | description = "Type system extensions for programs checked with the mypy type checker." 545 | optional = false 546 | python-versions = ">=3.5" 547 | files = [ 548 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 549 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 550 | ] 551 | 552 | [[package]] 553 | name = "packaging" 554 | version = "24.1" 555 | description = "Core utilities for Python packages" 556 | optional = false 557 | python-versions = ">=3.8" 558 | files = [ 559 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 560 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 561 | ] 562 | 563 | [[package]] 564 | name = "pathspec" 565 | version = "0.12.1" 566 | description = "Utility library for gitignore style pattern matching of file paths." 567 | optional = false 568 | python-versions = ">=3.8" 569 | files = [ 570 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 571 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 572 | ] 573 | 574 | [[package]] 575 | name = "platformdirs" 576 | version = "4.2.2" 577 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 578 | optional = false 579 | python-versions = ">=3.8" 580 | files = [ 581 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 582 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 583 | ] 584 | 585 | [package.extras] 586 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 587 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 588 | type = ["mypy (>=1.8)"] 589 | 590 | [[package]] 591 | name = "pluggy" 592 | version = "1.5.0" 593 | description = "plugin and hook calling mechanisms for python" 594 | optional = false 595 | python-versions = ">=3.8" 596 | files = [ 597 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 598 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 599 | ] 600 | 601 | [package.extras] 602 | dev = ["pre-commit", "tox"] 603 | testing = ["pytest", "pytest-benchmark"] 604 | 605 | [[package]] 606 | name = "pontos" 607 | version = "24.9.0" 608 | description = "Common utilities and tools maintained by Greenbone Networks" 609 | optional = false 610 | python-versions = "<4.0,>=3.9" 611 | files = [ 612 | {file = "pontos-24.9.0-py3-none-any.whl", hash = "sha256:ec0bb28f874a1d88c765a80ca77f217d5f799b3dfb6c266e25de6df365a142c0"}, 613 | {file = "pontos-24.9.0.tar.gz", hash = "sha256:7336142b382d5e0bb52146b7bbb2185de8e0be51509e1db32976578a6b1e7a91"}, 614 | ] 615 | 616 | [package.dependencies] 617 | colorful = ">=0.5.4" 618 | httpx = {version = ">=0.23", extras = ["http2"]} 619 | lxml = ">=4.9.0" 620 | packaging = ">=20.3" 621 | python-dateutil = ">=2.8.2" 622 | rich = ">=12.4.4" 623 | semver = ">=2.13" 624 | shtab = ">=1.7.0" 625 | tomlkit = ">=0.5.11" 626 | 627 | [[package]] 628 | name = "pydantic" 629 | version = "2.9.0" 630 | description = "Data validation using Python type hints" 631 | optional = false 632 | python-versions = ">=3.8" 633 | files = [ 634 | {file = "pydantic-2.9.0-py3-none-any.whl", hash = "sha256:f66a7073abd93214a20c5f7b32d56843137a7a2e70d02111f3be287035c45370"}, 635 | {file = "pydantic-2.9.0.tar.gz", hash = "sha256:c7a8a9fdf7d100afa49647eae340e2d23efa382466a8d177efcd1381e9be5598"}, 636 | ] 637 | 638 | [package.dependencies] 639 | annotated-types = ">=0.4.0" 640 | pydantic-core = "2.23.2" 641 | typing-extensions = [ 642 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 643 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 644 | ] 645 | tzdata = {version = "*", markers = "python_version >= \"3.9\""} 646 | 647 | [package.extras] 648 | email = ["email-validator (>=2.0.0)"] 649 | 650 | [[package]] 651 | name = "pydantic-core" 652 | version = "2.23.2" 653 | description = "Core functionality for Pydantic validation and serialization" 654 | optional = false 655 | python-versions = ">=3.8" 656 | files = [ 657 | {file = "pydantic_core-2.23.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7d0324a35ab436c9d768753cbc3c47a865a2cbc0757066cb864747baa61f6ece"}, 658 | {file = "pydantic_core-2.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:276ae78153a94b664e700ac362587c73b84399bd1145e135287513442e7dfbc7"}, 659 | {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c7aa318da542cdcc60d4a648377ffe1a2ef0eb1e996026c7f74507b720a78"}, 660 | {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cf842265a3a820ebc6388b963ead065f5ce8f2068ac4e1c713ef77a67b71f7c"}, 661 | {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae90b9e50fe1bd115b24785e962b51130340408156d34d67b5f8f3fa6540938e"}, 662 | {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ae65fdfb8a841556b52935dfd4c3f79132dc5253b12c0061b96415208f4d622"}, 663 | {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c8aa40f6ca803f95b1c1c5aeaee6237b9e879e4dfb46ad713229a63651a95fb"}, 664 | {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c53100c8ee5a1e102766abde2158077d8c374bee0639201f11d3032e3555dfbc"}, 665 | {file = "pydantic_core-2.23.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6b9dd6aa03c812017411734e496c44fef29b43dba1e3dd1fa7361bbacfc1354"}, 666 | {file = "pydantic_core-2.23.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b18cf68255a476b927910c6873d9ed00da692bb293c5b10b282bd48a0afe3ae2"}, 667 | {file = "pydantic_core-2.23.2-cp310-none-win32.whl", hash = "sha256:e460475719721d59cd54a350c1f71c797c763212c836bf48585478c5514d2854"}, 668 | {file = "pydantic_core-2.23.2-cp310-none-win_amd64.whl", hash = "sha256:5f3cf3721eaf8741cffaf092487f1ca80831202ce91672776b02b875580e174a"}, 669 | {file = "pydantic_core-2.23.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7ce8e26b86a91e305858e018afc7a6e932f17428b1eaa60154bd1f7ee888b5f8"}, 670 | {file = "pydantic_core-2.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e9b24cca4037a561422bf5dc52b38d390fb61f7bfff64053ce1b72f6938e6b2"}, 671 | {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753294d42fb072aa1775bfe1a2ba1012427376718fa4c72de52005a3d2a22178"}, 672 | {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:257d6a410a0d8aeb50b4283dea39bb79b14303e0fab0f2b9d617701331ed1515"}, 673 | {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8319e0bd6a7b45ad76166cc3d5d6a36c97d0c82a196f478c3ee5346566eebfd"}, 674 | {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a05c0240f6c711eb381ac392de987ee974fa9336071fb697768dfdb151345ce"}, 675 | {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d5b0ff3218858859910295df6953d7bafac3a48d5cd18f4e3ed9999efd2245f"}, 676 | {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96ef39add33ff58cd4c112cbac076726b96b98bb8f1e7f7595288dcfb2f10b57"}, 677 | {file = "pydantic_core-2.23.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0102e49ac7d2df3379ef8d658d3bc59d3d769b0bdb17da189b75efa861fc07b4"}, 678 | {file = "pydantic_core-2.23.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6612c2a844043e4d10a8324c54cdff0042c558eef30bd705770793d70b224aa"}, 679 | {file = "pydantic_core-2.23.2-cp311-none-win32.whl", hash = "sha256:caffda619099cfd4f63d48462f6aadbecee3ad9603b4b88b60cb821c1b258576"}, 680 | {file = "pydantic_core-2.23.2-cp311-none-win_amd64.whl", hash = "sha256:6f80fba4af0cb1d2344869d56430e304a51396b70d46b91a55ed4959993c0589"}, 681 | {file = "pydantic_core-2.23.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c83c64d05ffbbe12d4e8498ab72bdb05bcc1026340a4a597dc647a13c1605ec"}, 682 | {file = "pydantic_core-2.23.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6294907eaaccf71c076abdd1c7954e272efa39bb043161b4b8aa1cd76a16ce43"}, 683 | {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a801c5e1e13272e0909c520708122496647d1279d252c9e6e07dac216accc41"}, 684 | {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc0c316fba3ce72ac3ab7902a888b9dc4979162d320823679da270c2d9ad0cad"}, 685 | {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b06c5d4e8701ac2ba99a2ef835e4e1b187d41095a9c619c5b185c9068ed2a49"}, 686 | {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82764c0bd697159fe9947ad59b6db6d7329e88505c8f98990eb07e84cc0a5d81"}, 687 | {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b1a195efd347ede8bcf723e932300292eb13a9d2a3c1f84eb8f37cbbc905b7f"}, 688 | {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7efb12e5071ad8d5b547487bdad489fbd4a5a35a0fc36a1941517a6ad7f23e0"}, 689 | {file = "pydantic_core-2.23.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5dd0ec5f514ed40e49bf961d49cf1bc2c72e9b50f29a163b2cc9030c6742aa73"}, 690 | {file = "pydantic_core-2.23.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:820f6ee5c06bc868335e3b6e42d7ef41f50dfb3ea32fbd523ab679d10d8741c0"}, 691 | {file = "pydantic_core-2.23.2-cp312-none-win32.whl", hash = "sha256:3713dc093d5048bfaedbba7a8dbc53e74c44a140d45ede020dc347dda18daf3f"}, 692 | {file = "pydantic_core-2.23.2-cp312-none-win_amd64.whl", hash = "sha256:e1895e949f8849bc2757c0dbac28422a04be031204df46a56ab34bcf98507342"}, 693 | {file = "pydantic_core-2.23.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:da43cbe593e3c87d07108d0ebd73771dc414488f1f91ed2e204b0370b94b37ac"}, 694 | {file = "pydantic_core-2.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:64d094ea1aa97c6ded4748d40886076a931a8bf6f61b6e43e4a1041769c39dd2"}, 695 | {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:084414ffe9a85a52940b49631321d636dadf3576c30259607b75516d131fecd0"}, 696 | {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043ef8469f72609c4c3a5e06a07a1f713d53df4d53112c6d49207c0bd3c3bd9b"}, 697 | {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3649bd3ae6a8ebea7dc381afb7f3c6db237fc7cebd05c8ac36ca8a4187b03b30"}, 698 | {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6db09153d8438425e98cdc9a289c5fade04a5d2128faff8f227c459da21b9703"}, 699 | {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5668b3173bb0b2e65020b60d83f5910a7224027232c9f5dc05a71a1deac9f960"}, 700 | {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c7b81beaf7c7ebde978377dc53679c6cba0e946426fc7ade54251dfe24a7604"}, 701 | {file = "pydantic_core-2.23.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ae579143826c6f05a361d9546446c432a165ecf1c0b720bbfd81152645cb897d"}, 702 | {file = "pydantic_core-2.23.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:19f1352fe4b248cae22a89268720fc74e83f008057a652894f08fa931e77dced"}, 703 | {file = "pydantic_core-2.23.2-cp313-none-win32.whl", hash = "sha256:e1a79ad49f346aa1a2921f31e8dbbab4d64484823e813a002679eaa46cba39e1"}, 704 | {file = "pydantic_core-2.23.2-cp313-none-win_amd64.whl", hash = "sha256:582871902e1902b3c8e9b2c347f32a792a07094110c1bca6c2ea89b90150caac"}, 705 | {file = "pydantic_core-2.23.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:743e5811b0c377eb830150d675b0847a74a44d4ad5ab8845923d5b3a756d8100"}, 706 | {file = "pydantic_core-2.23.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6650a7bbe17a2717167e3e23c186849bae5cef35d38949549f1c116031b2b3aa"}, 707 | {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56e6a12ec8d7679f41b3750ffa426d22b44ef97be226a9bab00a03365f217b2b"}, 708 | {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810ca06cca91de9107718dc83d9ac4d2e86efd6c02cba49a190abcaf33fb0472"}, 709 | {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:785e7f517ebb9890813d31cb5d328fa5eda825bb205065cde760b3150e4de1f7"}, 710 | {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ef71ec876fcc4d3bbf2ae81961959e8d62f8d74a83d116668409c224012e3af"}, 711 | {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50ac34835c6a4a0d456b5db559b82047403c4317b3bc73b3455fefdbdc54b0a"}, 712 | {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16b25a4a120a2bb7dab51b81e3d9f3cde4f9a4456566c403ed29ac81bf49744f"}, 713 | {file = "pydantic_core-2.23.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:41ae8537ad371ec018e3c5da0eb3f3e40ee1011eb9be1da7f965357c4623c501"}, 714 | {file = "pydantic_core-2.23.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07049ec9306ec64e955b2e7c40c8d77dd78ea89adb97a2013d0b6e055c5ee4c5"}, 715 | {file = "pydantic_core-2.23.2-cp38-none-win32.whl", hash = "sha256:086c5db95157dc84c63ff9d96ebb8856f47ce113c86b61065a066f8efbe80acf"}, 716 | {file = "pydantic_core-2.23.2-cp38-none-win_amd64.whl", hash = "sha256:67b6655311b00581914aba481729971b88bb8bc7996206590700a3ac85e457b8"}, 717 | {file = "pydantic_core-2.23.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:358331e21a897151e54d58e08d0219acf98ebb14c567267a87e971f3d2a3be59"}, 718 | {file = "pydantic_core-2.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4d9f15ffe68bcd3898b0ad7233af01b15c57d91cd1667f8d868e0eacbfe3f87"}, 719 | {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0123655fedacf035ab10c23450163c2f65a4174f2bb034b188240a6cf06bb123"}, 720 | {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e6e3ccebdbd6e53474b0bb7ab8b88e83c0cfe91484b25e058e581348ee5a01a5"}, 721 | {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc535cb898ef88333cf317777ecdfe0faac1c2a3187ef7eb061b6f7ecf7e6bae"}, 722 | {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aab9e522efff3993a9e98ab14263d4e20211e62da088298089a03056980a3e69"}, 723 | {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05b366fb8fe3d8683b11ac35fa08947d7b92be78ec64e3277d03bd7f9b7cda79"}, 724 | {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7568f682c06f10f30ef643a1e8eec4afeecdafde5c4af1b574c6df079e96f96c"}, 725 | {file = "pydantic_core-2.23.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cdd02a08205dc90238669f082747612cb3c82bd2c717adc60f9b9ecadb540f80"}, 726 | {file = "pydantic_core-2.23.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a2ab4f410f4b886de53b6bddf5dd6f337915a29dd9f22f20f3099659536b2f6"}, 727 | {file = "pydantic_core-2.23.2-cp39-none-win32.whl", hash = "sha256:0448b81c3dfcde439551bb04a9f41d7627f676b12701865c8a2574bcea034437"}, 728 | {file = "pydantic_core-2.23.2-cp39-none-win_amd64.whl", hash = "sha256:4cebb9794f67266d65e7e4cbe5dcf063e29fc7b81c79dc9475bd476d9534150e"}, 729 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e758d271ed0286d146cf7c04c539a5169a888dd0b57026be621547e756af55bc"}, 730 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f477d26183e94eaafc60b983ab25af2a809a1b48ce4debb57b343f671b7a90b6"}, 731 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da3131ef2b940b99106f29dfbc30d9505643f766704e14c5d5e504e6a480c35e"}, 732 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329a721253c7e4cbd7aad4a377745fbcc0607f9d72a3cc2102dd40519be75ed2"}, 733 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7706e15cdbf42f8fab1e6425247dfa98f4a6f8c63746c995d6a2017f78e619ae"}, 734 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e64ffaf8f6e17ca15eb48344d86a7a741454526f3a3fa56bc493ad9d7ec63936"}, 735 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dd59638025160056687d598b054b64a79183f8065eae0d3f5ca523cde9943940"}, 736 | {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:12625e69b1199e94b0ae1c9a95d000484ce9f0182f9965a26572f054b1537e44"}, 737 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d813fd871b3d5c3005157622ee102e8908ad6011ec915a18bd8fde673c4360e"}, 738 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1eb37f7d6a8001c0f86dc8ff2ee8d08291a536d76e49e78cda8587bb54d8b329"}, 739 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce7eaf9a98680b4312b7cebcdd9352531c43db00fca586115845df388f3c465"}, 740 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f087879f1ffde024dd2788a30d55acd67959dcf6c431e9d3682d1c491a0eb474"}, 741 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ce883906810b4c3bd90e0ada1f9e808d9ecf1c5f0b60c6b8831d6100bcc7dd6"}, 742 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a8031074a397a5925d06b590121f8339d34a5a74cfe6970f8a1124eb8b83f4ac"}, 743 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23af245b8f2f4ee9e2c99cb3f93d0e22fb5c16df3f2f643f5a8da5caff12a653"}, 744 | {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c57e493a0faea1e4c38f860d6862ba6832723396c884fbf938ff5e9b224200e2"}, 745 | {file = "pydantic_core-2.23.2.tar.gz", hash = "sha256:95d6bf449a1ac81de562d65d180af5d8c19672793c81877a2eda8fde5d08f2fd"}, 746 | ] 747 | 748 | [package.dependencies] 749 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 750 | 751 | [[package]] 752 | name = "pygments" 753 | version = "2.18.0" 754 | description = "Pygments is a syntax highlighting package written in Python." 755 | optional = false 756 | python-versions = ">=3.8" 757 | files = [ 758 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 759 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 760 | ] 761 | 762 | [package.extras] 763 | windows-terminal = ["colorama (>=0.4.6)"] 764 | 765 | [[package]] 766 | name = "pytest" 767 | version = "7.4.4" 768 | description = "pytest: simple powerful testing with Python" 769 | optional = false 770 | python-versions = ">=3.7" 771 | files = [ 772 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 773 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 774 | ] 775 | 776 | [package.dependencies] 777 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 778 | iniconfig = "*" 779 | packaging = "*" 780 | pluggy = ">=0.12,<2.0" 781 | 782 | [package.extras] 783 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 784 | 785 | [[package]] 786 | name = "python-dateutil" 787 | version = "2.9.0.post0" 788 | description = "Extensions to the standard Python datetime module" 789 | optional = false 790 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 791 | files = [ 792 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 793 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 794 | ] 795 | 796 | [package.dependencies] 797 | six = ">=1.5" 798 | 799 | [[package]] 800 | name = "rich" 801 | version = "13.8.0" 802 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 803 | optional = false 804 | python-versions = ">=3.7.0" 805 | files = [ 806 | {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, 807 | {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, 808 | ] 809 | 810 | [package.dependencies] 811 | markdown-it-py = ">=2.2.0" 812 | pygments = ">=2.13.0,<3.0.0" 813 | 814 | [package.extras] 815 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 816 | 817 | [[package]] 818 | name = "ruff" 819 | version = "0.5.7" 820 | description = "An extremely fast Python linter and code formatter, written in Rust." 821 | optional = false 822 | python-versions = ">=3.7" 823 | files = [ 824 | {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, 825 | {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, 826 | {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, 827 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, 828 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, 829 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, 830 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, 831 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, 832 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, 833 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, 834 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, 835 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, 836 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, 837 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, 838 | {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, 839 | {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, 840 | {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, 841 | {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, 842 | ] 843 | 844 | [[package]] 845 | name = "semver" 846 | version = "3.0.2" 847 | description = "Python helper for Semantic Versioning (https://semver.org)" 848 | optional = false 849 | python-versions = ">=3.7" 850 | files = [ 851 | {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, 852 | {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, 853 | ] 854 | 855 | [[package]] 856 | name = "shtab" 857 | version = "1.7.1" 858 | description = "Automagic shell tab completion for Python CLI applications" 859 | optional = false 860 | python-versions = ">=3.7" 861 | files = [ 862 | {file = "shtab-1.7.1-py3-none-any.whl", hash = "sha256:32d3d2ff9022d4c77a62492b6ec875527883891e33c6b479ba4d41a51e259983"}, 863 | {file = "shtab-1.7.1.tar.gz", hash = "sha256:4e4bcb02eeb82ec45920a5d0add92eac9c9b63b2804c9196c1f1fdc2d039243c"}, 864 | ] 865 | 866 | [package.extras] 867 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout"] 868 | 869 | [[package]] 870 | name = "six" 871 | version = "1.16.0" 872 | description = "Python 2 and 3 compatibility utilities" 873 | optional = false 874 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 875 | files = [ 876 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 877 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 878 | ] 879 | 880 | [[package]] 881 | name = "sniffio" 882 | version = "1.3.1" 883 | description = "Sniff out which async library your code is running under" 884 | optional = false 885 | python-versions = ">=3.7" 886 | files = [ 887 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 888 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 889 | ] 890 | 891 | [[package]] 892 | name = "tomlkit" 893 | version = "0.13.2" 894 | description = "Style preserving TOML library" 895 | optional = false 896 | python-versions = ">=3.8" 897 | files = [ 898 | {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, 899 | {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, 900 | ] 901 | 902 | [[package]] 903 | name = "typing-extensions" 904 | version = "4.12.2" 905 | description = "Backported and Experimental Type Hints for Python 3.8+" 906 | optional = false 907 | python-versions = ">=3.8" 908 | files = [ 909 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 910 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 911 | ] 912 | 913 | [[package]] 914 | name = "tzdata" 915 | version = "2024.1" 916 | description = "Provider of IANA time zone data" 917 | optional = false 918 | python-versions = ">=2" 919 | files = [ 920 | {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, 921 | {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, 922 | ] 923 | 924 | [metadata] 925 | lock-version = "2.0" 926 | python-versions = "^3.12" 927 | content-hash = "9457eda4077bfb31f2f2b133ff340df9deed23fc4a659e965d504d85c6b06b49" 928 | --------------------------------------------------------------------------------