diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 95676a5..11265a6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,28 +11,28 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - - name: Install uv for package management - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - uv pip install --system ruff - uv pip install --system -r pyproject.toml - - - uses: chartboost/ruff-action@v1 - - - name: Test with pytest - run: | - uv run tests/*.py + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv for package management + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + uv pip install --system ruff + uv pip install --system -r pyproject.toml + + - uses: chartboost/ruff-action@v1 + + - name: Test with pytest + run: | + uv run pytest tests/*.py diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 8066d8a..194db01 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -11,38 +11,108 @@ name: Upload Python Package on: release: types: [published] + pull_request: permissions: contents: read + id-token: write jobs: - deploy: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Update version in pyproject.toml + run: | + echo "Updating version in pyproject.toml" + LAST_RELEASE=$(pip index \ + -i https://test.pypi.org/simple/ \ + versions adaptive-cards-py 2>/dev/null \ + | egrep -o '([0-9]+\.){2}[0-9]+' | head -n 1) + NEW_RELEASE=$(echo "$LAST_RELEASE" | awk -F. -v OFS=. '{$NF += 1; print}') + sed -i "s/version = \".*\"/version = \"${NEW_RELEASE}\"/" pyproject.toml + if: github.event_name == 'pull_request' + - name: Update version in pyproject.toml + run: | + echo "Updating version in pyproject.toml" + TAG_NAME=${GITHUB_REF##*/} # Get the tag name from GITHUB_REF + # Optionally, you may want to sanitize TAG_NAME (remove 'v' prefix, etc.) + sed -i "s/version = \".*\"/version = \"${TAG_NAME}\"/" pyproject.toml + if: github.event_name == 'release' + + - name: Build package + run: uv build + + - uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: dist/ + + deploy-test: + if: github.event_name == 'pull_request' + needs: build runs-on: ubuntu-latest + environment: test + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + - uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: dist/ + + - name: Publish package + run: | + cat pyproject.toml + uv publish \ + --check-url https://test.pypi.org/simple/ \ + --publish-url https://test.pypi.org/legacy/ \ + --trusted-publishing always + + deploy-release: + if: github.event_name == 'release' + needs: build + runs-on: ubuntu-latest + environment: release steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - - name: Update version in pyproject.toml - id: update_version - run: | - echo "Updating version in pyproject.toml" - TAG_NAME=${GITHUB_REF##*/} # Get the tag name from GITHUB_REF - # Optionally, you may want to sanitize TAG_NAME (remove 'v' prefix, etc.) - sed -i "s/version = \".*\"/version = \"${TAG_NAME}\"/" pyproject.toml - - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + - uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: dist/ + + - name: Publish package + run: uv publish --trusted-publishing always diff --git a/.gitignore b/.gitignore index f956608..1800114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,174 @@ -cards -.vscode -__pycache__ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env .venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc \ No newline at end of file diff --git a/README.md b/README.md index eb4353e..8094bd2 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,26 @@ -# Adaptive Cards - -- [About](#about) -- [Features](#features) -- [Dependencies](#dependencies) -- [Installation](#installation) -- [Library structure](#library-structure) -- [Usage](#usage) - - [A simple card](#a-simple-card) - - [Adding multiple elements at once](#adding-multiple-elements-at-once) - - [A more complex card](#a-more-complex-card) - - [Validate schema](#validate-schema) - - [Send card to MS Teams](#send-card-to-ms-teams) -- [Examples](#examples) -- [Feature Roadmap](#feature-roadmap) -- [Contribution](#contribution) +# Adaptive Cards + +- [Adaptive Cards ](#adaptive-cards-) + - [About](#about) + - [Features](#features) + - [Dependencies](#dependencies) + - [Installation](#installation) + - [Library structure](#library-structure) + - [Usage](#usage) + - [Create a card](#create-a-card) + - [A simple card](#a-simple-card) + - [A more complex card](#a-more-complex-card) + - [Update card components](#update-card-components) + - [Validate a card](#validate-a-card) + - [Send card to MS Teams](#send-card-to-ms-teams) + - [Examples](#examples) + - [Feature Roadmap](#feature-roadmap) + - [Contribution](#contribution) + - [Glossary](#glossary) [![PyPI version](https://badge.fury.io/py/adaptive-cards-py.svg)](https://pypi.org/project/adaptive-cards-py/) -A thin Python wrapper for creating [**Adaptive Cards**](https://adaptivecards.io/) easily on code level. The deep integration of Python's `typing` package prevents you from creating invalid schemas and guides you while setting up the code for generating visual appealing cards. +A thin Python wrapper for creating [**Adaptive Cards**](https://adaptivecards.io/) easily on code level. The deep integration of Python's `typing` package alongside the famous `pydantic` library prevents you from creating invalid schemas and guides you while setting up the code for generating visually appealing cards. If you are interested in the general concepts of adaptive cards and want to dig a bit deeper, have a look into the [**official documentation**](https://learn.microsoft.com/en-us/adaptive-cards/) or get used to the [**schema**](https://adaptivecards.io/explorer/) first. @@ -26,7 +29,7 @@ If you are interested in the general concepts of adaptive cards and want to dig ## About -This library is intended to provide a clear and simple interface for creating adaptive cards with only a few lines of code in a more robust way. The heavy usage of Python's `typing` library should prevent one from creating invalid schemes and structures. Instead, creating and sending cards should be intuitive and be supported by the typing system. +This library is intended to provide a clear and simple interface for creating adaptive cards with only a few lines of code in a more robust way. The heavy usage of Python's `typing` mechanisms and the `pydantic` library should prevent one from creating invalid schemes and structures. Instead, creating and sending cards should be intuitive and supported by the typing system. For a comprehensive introduction into the main ideas and patterns of adaptive cards, head over to the [**official documentation**](https://docs.microsoft.com/en-us/adaptive-cards). I also recommend using the [**schema explorer**](https://adaptivecards.io/explorer) page alongside the implementation, since the library's type system relies on these schemas. @@ -36,39 +39,50 @@ Further resources can be found here: * [__Official repository__](https://github.com/microsoft/AdaptiveCards) 💡 **Please note** -
There are size limitations related to the [__target framework__](https://learn.microsoft.com/en-us/adaptive-cards/resources/partners#live) (or "__Host__") a card is supposed to be used with. As of now, the maximum card size can be __28KB__ when used with Webhooks in Teams ([__Format cards in Teams__](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cdesktop1%2Cdesktop2%2Cconnector-html)). For bot frameworks the upper limit is set to __40KB__ ([__Format your bot messages__](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/format-your-bot-messages)). An corresponding check is planned to be added soon to the [`SchemaValidator`](#validate-schema). +
There are size limitations related to the [__target framework__](https://learn.microsoft.com/en-us/adaptive-cards/resources/partners#live) (or "__Host__") a card is supposed to be used with. As of now, the maximum card size can be __28KB__ when used with Webhooks in Teams ([__Format cards in Teams__](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cdesktop1%2Cdesktop2%2Cconnector-html)). For bot frameworks the upper limit is set to __40KB__ ([__Format your bot messages__](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/format-your-bot-messages)). An corresponding check is part of the [`CardValidator`](#validate-schema). ## Features 💡 **Please note** -
It's highly recommended to turn on the **type check** capabilities for Python in your editor. This will serve you with direct feedback about the structures you create. If you are trying to assign values of incompatible types, your editor will mark it as such and yell at you right in the moment you are about to do so. Otherwise, invalid schemas can be detected by making use of the card validation, -once the card has been successfully created. +
It's highly recommended to turn on the **type check** capabilities for Python in your editor. This will serve you with direct feedback about the structures you create. If you are trying to assign values of incompatible types, your editor will mark it as such and yell at you right in the moment you are about to do so. Otherwise, invalid schemas can be detected by making use of the card validation, once the card has been successfully created. Cards are validated against the official schema there and possible incompatibilities can be detected. -+ Type annotated components based on Python's **dataclasses** -+ Schema validation for version compatibility -+ Simple `JSON` export +**Key aspects** + ++ Type annotated components based on `pydantic` and `typing` ++ Validation of versions, card size and schema ++ Simple `json` and `dict` export ++ Update methods for manipulating card components after creation ++ [**Passive error handling**](https://github.com/rustedpy/result) via the `result` package for validation and card updates (similar to Rust approach) + Compliant with the official structures and ideas -+ Send cards to MS Teams via `TeamsClient` ++ Communication via `TeamsClient` ## Dependencies -* Python 3.10+ -* `dataclasses-json` +* `pydantic` * `requests` +* `jsonschema` +* `mypy` +* `result` + +Works with Python 3.10+ ## Installation ```bash pip install adaptive-cards-py ``` +or +```bash +uv add adaptive-cards-py +``` ## Library structure **Adaptive cards** can consist of different kinds of components. The four main categories beside the actual cards are **Elements**, **Containers**, **Actions** and **Inputs**. You can find all available components for each category within the corresponding module. The `AdaptiveCard` is defined in the `cards` module. -In addition to that, some fields of certain components are of custom types. These types are living inside the `card_types` mpdule. For instance, if you are about to assign a color to a `TextBlock`, the field `color` will only accept a value of type `Colors`, which is implemented in the aforementioned Python file. +In addition to that, some fields of certain components are of custom types. These types are living inside the `card_types` module. For instance, if you are about to assign a color to a `TextBlock`, the field `color` will only accept a value of type `Colors`, which is implemented in the aforementioned Python file. -To perform validation on a fully initialized card, one can make use of the `CardValidator` class (`validation` module). Similar to the whole library, this class provides a simple interface. For creating a validator, a Factory (`CardValidatorFactory`) can be used, in order to account for the desired target framework. Validation will check the following points: +To perform validation on a fully initialized card, one can make use of the `CardValidator` class (`validation` module). Similar to the whole library, this class provides a simple interface. For creating a validator, a factory (`CardValidatorFactory`) can be used, in order to account for the desired target framework. Validation will check the following points: * Are any components used, which are not yet available for the card version? @@ -78,7 +92,9 @@ To perform validation on a fully initialized card, one can make use of the `Card ## Usage -### A simple card +### Create a card + +#### A simple card A simple `TextBlock` lives in the `elements` module and can be used after it's import. @@ -120,7 +136,7 @@ Find your final layout below. ![simple card](https://github.com/dennis6p/adaptive-cards-py/blob/main/examples/simple_card/simple_card.jpg?raw=true) 💡 **Please note** -
After building the object is done, the `create(...)` method must be called in order to create the final object. In this case, the object will be of type `AdaptiveCard`. +
After building the object is done, the `create(...)` method must be called in order to construct the final object. In this case, the object will be of type `AdaptiveCard`. To directly export your result, make use of the `to_json()` method provided by every card. @@ -131,9 +147,7 @@ with open("path/to/out/file.json", "w+") as f: ``` -### Adding multiple elements at once - -Assuming you have a bunch of elements you want your card to enrich with. There is also a method for doing so. Let's re-use the example from before, but add another `Image` element here as well. +Assuming you have a bunch of components you want your card to enrich with. There is also a method for doing so. Let's re-use the example from before, but add another `Image` component here as well. ```Python from adaptive_cards.elements import TextBlock, Image @@ -170,7 +184,7 @@ This will result in a card like shown below. ![simple card 2](https://github.com/dennis6p/adaptive-cards-py/blob/main/examples/simple_card/simple_card_2.jpg?raw=true) -### A more complex card +#### A more complex card You can have a look on the following example for getting an idea of what's actually possible with adaptive cards. @@ -182,10 +196,11 @@ You can have a look on the following example for getting an idea of what's actua ```python import adaptive_cards.card_types as types from adaptive_cards.actions import ActionToggleVisibility, TargetElement -from adaptive_cards.validation import SchemaValidator, Result +from adaptive_cards.validation import SchemaValidator from adaptive_cards.card import AdaptiveCard from adaptive_cards.elements import TextBlock, Image from adaptive_cards.containers import Container, ContainerTypes, ColumnSet, Column +from result import Result, Ok, Err, is_ok containers: list[ContainerTypes] = [] @@ -334,9 +349,9 @@ containers.append( card = AdaptiveCard.new().version("1.5").add_items(containers).create() validator: SchemaValidator = SchemaValidator() -result: Result = validator.validate(card) +result: Result[None, str] = validator.validate(card) -print(f"Validation was successful: {result == Result.SUCCESS}") +print(f"Validation was successful: {is_ok(result)}") ``` @@ -598,17 +613,57 @@ print(f"Validation was successful: {result == Result.SUCCESS}") -### Validate schema +### Update card components -New components and fields are getting introduced every now and then. This means, if you are using an early version for a card and add fields, which are not compliant with it, you will have an invalid schema. To prevent you from exporting fields not yet supported by the card and target framework, a card validation can be performed for the expected [__target framework__](https://learn.microsoft.com/en-us/adaptive-cards/resources/partners#live) (see [__Library structure__](#library-structure) for more info). For MS Teams as the target framework, it would like like this: +Updating components and their fields can be done in-place via the `update_item(...)`/`update_action(...)` methods executed on a card object. Please note, that this is only possible for components which got assigned a proper `id` and have been part of the **initial** card setup. IDs for components added to the layout via the update method are not tracked. Hence, updating these components won't have any effect! ```python -from adaptive_cards.validator import ( +import adaptive_cards.card_types as types +from adaptive_cards.actions import ActionOpenUrl +from adaptive_cards.card import AdaptiveCard +from adaptive_cards.elements import TextBlock + +text_block: TextBlock = TextBlock( + id="text-id", + text="Initial text", +) + +action_open_url: ActionOpenUrl = ActionOpenUrl( + id="action-id", url="any-url", title="title" +) + +# build card +version: str = "1.4" +card: AdaptiveCard = AdaptiveCard.new().add_item(text_block).create() + +# update card +card.update_item( + id="text-id", + text="New text", + horizontal_alignment=types.HorizontalAlignment.CENTER, +) +card.update_action(id="action-id", url="new-url") +``` + +Updates will **only succeed** if the following three conditions are fulfilled: +- ID of an component has been set when the card was created initially +- The property about to be updated must be part of the actual data model of the parent component +- The property's type must match the defined type in the parent data model + +Properties do not need to be set initially to be updated via the above described steps. + +### Validate a card + +New components and properties are getting introduced every now and then. This means, if you are using an early version for a card and add properties, which are not compliant with it, you will have an invalid schema. To prevent you from exporting properties not yet supported by the card and target framework, a card validation can be performed for the expected [__target framework__](https://learn.microsoft.com/en-us/adaptive-cards/resources/partners#live) (see [__Library structure__](#library-structure) for more info). For MS Teams as the target framework, it would look like this: + +```python +from adaptive_cards.card import AdaptiveCard +from adaptive_cards.validation import ( CardValidatorFactory, - CardValidator, - Result, + CardValidator, Finding ) +from result import Result, Err, Ok, is_ok ... @@ -620,9 +675,9 @@ card: AdaptiveCard = AdaptiveCard.new() \ # generate a validator object for your required target framework validator: CardValidator = CardValidatorFactory.create_validator_microsoft_teams() -result: Result = validator.validate(card) +result: Result[None, str] = validator.validate(card) -print(f"Validation was successful: {result == Result.SUCCESS}") +print(f"Validation was successful: {is_ok(result)}") # As it might come in handy in some situations, there is a separate class method # which can be utilized to calculate the card size without running the full @@ -678,10 +733,17 @@ If you are interested in more comprehensive examples or the actual source code, * [x] Add size check to schema validation * [x] Add proper schema validation * [x] Add further target framework validators -* [ ] Provide more examples +* [x] Update card components after creation * [ ] Allow reading of json-like schemas ## Contribution Feel free to create issues, fork the repository or even come up with a pull request. I am happy about any kind of contribution and would love -to hear your feedback! +to hear your feedback or ideas for enhancement! + +## Glossary + +* **Item**: Any object of type *container*, *element* or *input* +* **Action**: Any object of type *action* +* **Component**: Synonym for both actions and items +* **Property**: A specific attribute a component comes with. Defined via the [**official schema**](https://adaptivecards.io/explorer/). diff --git a/adaptive_cards/actions.py b/adaptive_cards/actions.py deleted file mode 100644 index d3927f0..0000000 --- a/adaptive_cards/actions.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Implementations for all adaptive card action types""" - -from dataclasses import dataclass, field -from typing import Any, Optional, Union - -from dataclasses_json import LetterCase, dataclass_json - -import adaptive_cards.card_types as ct -from adaptive_cards import utils - -ActionTypes = Union[ - "ActionOpenUrl", - "ActionSubmit", - "ActionShowCard", - "ActionToggleVisibility", - "ActionExecute", -] -SelectAction = Union[ - "ActionExecute", "ActionOpenUrl", "ActionSubmit", "ActionToggleVisibility" -] - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Action: - # pylint: disable=too-many-instance-attributes - """ - Represents an action that can be performed. - - Attributes: - title: An optional string representing the title of the action. - icon_url: An optional string representing the URL of the icon associated with the action. - id: An optional string representing the ID of the action. - style: An optional ActionStyle enum value representing the style of the action. - fallback: An optional fallback ActionTypes object representing the fallback action to be - performed. - tooltip: An optional string representing the tooltip text for the action. - is_enabled: An optional boolean indicating whether the action is enabled or disabled. - mode: An optional ActionMode enum value representing the mode of the action. - requires: An optional dictionary mapping string keys to string values representing the - requirements for the action. - """ - - title: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - icon_url: Optional[str] = field(default=None, metadata=utils.get_metadata("1.1")) - id: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - style: Optional[ct.ActionStyle] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - fallback: Optional[ActionTypes] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - tooltip: Optional[str] = field(default=None, metadata=utils.get_metadata("1.5")) - is_enabled: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.5")) - mode: Optional[ct.ActionMode] = field( - default=ct.ActionMode.PRIMARY, metadata=utils.get_metadata("1.5") - ) - requires: Optional[dict[str, str]] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ActionOpenUrl(Action): - """ - Represents an action to open a URL. - - Inherits from Action - - Attributes: - url: The URL to be opened. - type: The type of the action. Default is "Action.OpenUrl". - """ - - url: str = field(metadata=utils.get_metadata("1.0")) - type: str = field(default="Action.OpenUrl", metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ActionSubmit(Action): - """ - Represents an action to submit data. - - Inherits from Action. - - Attributes: - type: The type of the action. Default is "Action.Submit". - data: Optional data associated with the action. - associated_inputs: Optional associated inputs for the action. - """ - - type: str = field(default="Action.Submit", metadata=utils.get_metadata("1.0")) - data: Optional[str | Any] = field(default=None, metadata=utils.get_metadata("1.0")) - associated_inputs: Optional[ct.AssociatedInputs] = field( - default=None, metadata=utils.get_metadata("1.3") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ActionShowCard(Action): - """ - Represents an action to show a card. - - Inherits from Action. - - Attributes: - type: The type of the action. Default is "Action.ShowCard". - card: Optional card to show. - """ - - type: str = field(default="Action.ShowCard", metadata=utils.get_metadata("1.0")) - card: Optional[Any] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TargetElement: - """ - Represents a target element. - - Attributes: - element_id: The ID of the target element. - is_visible: Optional flag indicating the visibility of the target element. - """ - - element_id: str = field(metadata=utils.get_metadata("1.0")) - is_visible: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ActionToggleVisibility(Action): - """ - Represents an action that toggles the visibility of target elements. - - Inherits from Action. - - Attributes: - target_elements: A list of TargetElement objects representing the target elements to toggle. - type: The type of the action, set to "Action.ToggleVisibility". - """ - - target_elements: list[TargetElement] = field(metadata=utils.get_metadata("1.2")) - type: str = field( - default="Action.ToggleVisibility", metadata=utils.get_metadata("1.2") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ActionExecute(Action): - """ - Represents an action that executes a command or performs an action. - - Inherits from Action. - - Attributes: - type: The type of the action, set to "Action.ShowCard". - verb: An optional string representing the verb of the action. - data: An optional string or Any type representing additional data associated - with the action. - associated_inputs: An optional AssociatedInputs object representing associated - inputs for the action. - """ - - type: str = field(default="Action.ShowCard", metadata=utils.get_metadata("1.4")) - verb: Optional[str] = field(default=None, metadata=utils.get_metadata("1.4")) - data: Optional[str | Any] = field(default=None, metadata=utils.get_metadata("1.4")) - associated_inputs: Optional[ct.AssociatedInputs] = field( - default=None, metadata=utils.get_metadata("1.4") - ) diff --git a/adaptive_cards/containers.py b/adaptive_cards/containers.py deleted file mode 100644 index 0181e89..0000000 --- a/adaptive_cards/containers.py +++ /dev/null @@ -1,364 +0,0 @@ -"""Implementations for all adaptive card container types""" - -from dataclasses import dataclass, field -from typing import Optional, Union - -from dataclasses_json import LetterCase, dataclass_json - -import adaptive_cards.actions as action -import adaptive_cards.card_types as ct -from adaptive_cards import elements, inputs, utils - -ContainerTypes = Union[ - "ActionSet", "Container", "ColumnSet", "FactSet", "ImageSet", "Table" -] - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ContainerBase: - """ - The ContainerBase class represents a base container for elements with various properties. - - Attributes: - fallback: The fallback element, if any, to be displayed when the container - cannot be rendered. - separator: Determines whether a separator should be shown above the container. - spacing: The spacing style to be applied within the container. - id: The unique identifier of the container. - is_visible: Determines whether the container is visible. - requires: A dictionary of requirements that must be satisfied for the container - to be displayed. - height: The height style to be applied to the container. - """ - - fallback: Optional[elements.Element | action.ActionTypes | inputs.InputTypes] = ( - field(default=None, metadata=utils.get_metadata("1.2")) - ) - separator: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - spacing: Optional[ct.Spacing] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - # pylint: disable=C0103 - id: Optional[str] = field(default=None, metadata=utils.get_metadata("1.2")) - is_visible: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - requires: Optional[dict[str, str]] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - height: Optional[ct.BlockElementHeight] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ActionSet(ContainerBase): - """Represents an action set, a container for a list of actions. - - Inherits from ContainerBase. - - Attributes: - actions: A list of actions in the action set. - type: The type of the action set. Defaults to "ActionSet". - """ - - actions: list[action.ActionTypes] = field(metadata=utils.get_metadata("1.2")) - type: str = field(default="ActionSet", metadata=utils.get_metadata("1.2")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Container(ContainerBase): - # pylint: disable=too-many-instance-attributes - """Represents a container for elements with various properties. - - Inherits from ContainerBase. - - Attributes: - items: A list of elements, sub-containers, or input elements contained within the container. - type: The type of the container. Defaults to "Container". - select_action: An optional select action associated with the container. - style: The style of the container. - vertical_content_alignment: The vertical alignment of the container's content. - bleed: Determines whether the container bleeds beyond its boundary. - background_image: The background image of the container. - min_height: The minimum height of the container. - rtl: Determines whether the container's content is displayed right-to-left. - """ - - items: list[elements.Element | ContainerTypes | inputs.InputTypes] = field( - metadata=utils.get_metadata("1.0") - ) - type: str = field(default="Container", metadata=utils.get_metadata("1.0")) - select_action: Optional[action.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - style: Optional[ct.ContainerStyle] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - vertical_content_alignment: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - bleed: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - background_image: Optional[ct.BackgroundImage | str] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - min_height: Optional[str] = field(default=None, metadata=utils.get_metadata("1.2")) - rtl: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.5")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ColumnSet(ContainerBase): - """Represents a set of columns within a container. - - Inherits from ContainerBase. - - Attributes: - type: The type of the column set. Defaults to "ColumnSet". - columns: An optional list of Column objects within the column set. - select_action: An optional select action associated with the column set. - style: The style of the column set. - bleed: Determines whether the column set bleeds beyond its boundary. - min_height: The minimum height of the column set. - horizontal_alignment: The horizontal alignment of the column set. - """ - - type: str = field(default="ColumnSet", metadata=utils.get_metadata("1.0")) - columns: Optional[list["Column"]] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - select_action: Optional[action.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - style: Optional[ct.ContainerStyle] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - bleed: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - min_height: Optional[str] = field(default=None, metadata=utils.get_metadata("1.2")) - horizontal_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Column(ContainerBase): - # pylint: disable=too-many-instance-attributes - """Represents a column within a container. - - Inherits from ContainerBase. - - Attributes: - type: The type of the column. Defaults to "Column". - items: An optional list of elements contained within the column. - background_image: The background image of the column. - bleed: Determines whether the column bleeds beyond its boundary. - min_height: The minimum height of the column. - rtl: Determines whether the column's content is displayed right-to-left. - separator: Determines whether a separator should be shown above the column. - spacing: The spacing style to be applied within the column. - select_action: An optional select action associated with the column. - style: The style of the column. - vertical_content_alignment: The vertical alignment of the column's content. - width: The width of the column. - """ - - type: str = field(default="Column", metadata=utils.get_metadata("1.0")) - items: Optional[list[elements.Element | ContainerTypes | inputs.Input]] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - background_image: Optional[ct.BackgroundImage | str] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - bleed: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - min_height: Optional[str] = field(default=None, metadata=utils.get_metadata("1.2")) - rtl: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.5")) - separator: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.0")) - spacing: Optional[ct.Spacing] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - select_action: Optional[action.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - style: Optional[ct.ContainerStyle] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - vertical_content_alignment: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - width: Optional[str | int] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class FactSet(ContainerBase): - """Represents a set of facts within a container. - - Inherits from ContainerBase. - - Attributes: - facts: A list of Fact objects within the fact set. - type: The type of the fact set. Defaults to "FactSet". - """ - - facts: list["Fact"] = field(metadata=utils.get_metadata("1.0")) - type: str = field(default="FactSet", metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Fact: - """Represents a fact. - - Attributes: - title: The title of the fact. - value: The value of the fact. - """ - - title: str = field(metadata=utils.get_metadata("1.0")) - value: str = field(metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class ImageSet(ContainerBase): - """Represents a set of images within a container. - - Inherits from ContainerBase. - - Attributes: - images: A list of Image objects within the image set. - type: The type of the image set. Defaults to "ImageSet". - image_size: The size of the images within the image set. - """ - - images: list[elements.Image] = field(metadata=utils.get_metadata("1.0")) - type: str = field(default="ImageSet", metadata=utils.get_metadata("1.2")) - image_size: Optional[ct.ImageSize] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TableColumnDefinition: - """Represents a definition for a table column. - - Attributes: - horizontal_cell_content_alignment: The horizontal alignment of cell content. - vertical_cell_content_alignment: The vertical alignment of cell content. - width: The width of the table column. - """ - - horizontal_cell_content_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - vertical_cell_content_alignment: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - width: Optional[str | int] = field(default=None, metadata=utils.get_metadata("1.5")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TableRow: - """Represents a row within a table. - - Attributes: - cells: The cells within the table row. - horizontal_cell_content_alignment: The horizontal alignment of cell content. - vertical_cell_content_alignment: The vertical alignment of cell content. - style: The style of the table row. - """ - - type: str = field(default="TableRow", metadata=utils.get_metadata("1.5")) - cells: Optional[list["TableCell"]] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - horizontal_cell_content_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - vertical_cell_content_alignment: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - style: Optional[str] = field(default=None, metadata=utils.get_metadata("1.5")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Table(ContainerBase): - # pylint: disable=too-many-instance-attributes - """Represents a table within a container. - - Inherits from ContainerBase. - - Attributes: - type: The type of the table. Defaults to "Table". - columns: The column definitions of the table. - rows: The rows of the table. - first_row_as_header: Whether the first row should be treated as a header. - show_grid_lines: Whether to show grid lines in the table. - grid_style: The style of the table grid. - horizontal_cell_content_alignment: The horizontal alignment of cell content. - vertical_cell_content_alignment: The vertical alignment of cell content. - """ - - type: str = field(default="Table", metadata=utils.get_metadata("1.5")) - columns: Optional[list[TableColumnDefinition]] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - rows: Optional[list[TableRow]] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - first_row_as_header: Optional[bool] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - show_grid_lines: Optional[bool] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - grid_style: Optional[ct.ContainerStyle] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - horizontal_cell_content_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - vertical_cell_content_alignment: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TableCell: - # pylint: disable=too-many-instance-attributes - """Represents a cell within a table. - - Attributes: - items: The elements within the cell. - select_action: The action to perform when the cell is selected. - style: The style of the cell. - vertical_content_alignment: The vertical alignment of cell content. - bleed: Whether the cell should bleed beyond its boundaries. - background_image: The background image of the cell. - min_height: The minimum height of the cell. - rtl: Whether the cell should be rendered in right-to-left direction. - """ - - type: str = field(default="TableCell", metadata=utils.get_metadata("1.5")) - items: list[elements.Element] = field(metadata=utils.get_metadata("1.5")) - select_action: Optional[action.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - style: Optional[ct.ContainerStyle] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - vertical_content_alignment: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - bleed: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - background_image: Optional[ct.BackgroundImage | str] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - min_height: Optional[str] = field(default=None, metadata=utils.get_metadata("1.2")) - rtl: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.5")) diff --git a/adaptive_cards/elements.py b/adaptive_cards/elements.py deleted file mode 100644 index 86fcdd2..0000000 --- a/adaptive_cards/elements.py +++ /dev/null @@ -1,257 +0,0 @@ -"""Implementations for adaptive card element types""" - -from dataclasses import dataclass, field -from typing import Union, Optional, Any -from dataclasses_json import LetterCase, dataclass_json - -from adaptive_cards import actions -from adaptive_cards import utils -import adaptive_cards.card_types as ct - -Element = Union["Image", "TextBlock", "Media", "CaptionSource", "RichTextBlock"] - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class CardElement: - """ - Represents a card element. - - Attributes: - Element: The element of the card. - separator: Indicates whether a separator should be displayed before the element. - spacing: The spacing for the element. - id: The ID of the element. - is_visible: Indicates whether the element is visible. - requires: The requirements for the element. - height: The height of the element. - """ - - element: Optional[Any | Element] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - separator: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.0")) - spacing: Optional[ct.Spacing] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - id: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - is_visible: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - requires: Optional[dict[str, str]] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - height: Optional[ct.BlockElementHeight] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TextBlock(CardElement): - # pylint: disable=too-many-instance-attributes - """ - Represents a text block card element. - - Inherits from CardElement. - - Attributes: - text: The text content of the text block. - type: The type of the card element. - color: The color of the text block. - font_type: The font type of the text block. - horizontal_alignment: The horizontal alignment of the text block. - is_subtle: Indicates whether the text block has subtle styling. - max_lines: The maximum number of lines to display for the text block. - size: The font size of the text block. - weight: The font weight of the text block. - wrap: Indicates whether the text should wrap within the text block. - style: The style of the text block. - """ - - text: str = field(metadata=utils.get_metadata("1.0")) - type: str = field(default="TextBlock", metadata=utils.get_metadata("1.0")) - color: Optional[ct.Colors] = field(default=None, metadata=utils.get_metadata("1.0")) - font_type: Optional[ct.FontType] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - horizontal_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - is_subtle: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.0")) - max_lines: Optional[int] = field(default=None, metadata=utils.get_metadata("1.0")) - size: Optional[ct.FontSize] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - weight: Optional[ct.FontWeight] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - wrap: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.0")) - style: Optional[ct.TextBlockStyle] = field( - default=None, metadata=utils.get_metadata("1.5") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Image(CardElement): - # pylint: disable=too-many-instance-attributes - """ - Represents an image card element. - - Inherits from CardElement. - - Attributes: - url: The URL of the image. - type: The type of the card element. - alt_text: The alternative text for the image. - background_color: The background color of the image. - horizontal_alignment: The horizontal alignment of the image. - select_action: The select action associated with the image. - size: The size of the image. - style: The style of the image. - width: The width of the image. - """ - - url: str = field(metadata=utils.get_metadata("1.0")) - type: str = field(default="Image", metadata=utils.get_metadata("1.0")) - alt_text: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - background_color: Optional[str] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - horizontal_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - select_action: Optional[actions.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - size: Optional[ct.ImageSize] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - style: Optional[ct.ImageStyle] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - width: Optional[str] = field(default=None, metadata=utils.get_metadata("1.1")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Media(CardElement): - """ - Represents a media card element. - - Inherits from CardElement. - - Attributes: - type: The type of the card element. - sources: The list of media sources. - poster: The poster image URL. - alt_text: The alternative text for the media. - caption_sources: The list of caption sources. - """ - - type: str = field(default="Media", metadata=utils.get_metadata("1.1")) - sources: list["MediaSource"] = field(metadata=utils.get_metadata("1.1")) - poster: Optional[str] = field(default=None, metadata=utils.get_metadata("1.1")) - alt_text: Optional[str] = field(default=None, metadata=utils.get_metadata("1.1")) - caption_sources: Optional[list["CaptionSource"]] = field( - default=None, metadata=utils.get_metadata("1.6") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class MediaSource: - """ - Represents a media source. - - Attributes: - url: The URL of the media source. - mime_type: The MIME type of the media source. - """ - - url: str = field(metadata=utils.get_metadata("1.1")) - mime_type: Optional[str] = field(default=None, metadata=utils.get_metadata("1.1")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class CaptionSource: - """ - Represents a caption source. - - Attributes: - mime_type: The MIME type of the caption source. - url: The URL of the caption source. - label: The label of the caption source. - """ - - mime_type: str = field(metadata=utils.get_metadata("1.6")) - url: str = field(metadata=utils.get_metadata("1.6")) - label: str = field(metadata=utils.get_metadata("1.6")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class RichTextBlock(CardElement): - """ - Represents a rich text block. - - Inherits from CardElement. - - Attributes: - inlines: A list of inlines in the rich text block. Each inline can be a string - or a TextRun object. - type: The type of the rich text block. - horizontal_alignment: The horizontal alignment of the rich text block. - """ - - inlines: list[Union[str, "TextRun"]] = field(metadata=utils.get_metadata("1.2")) - type: str = field(default="RichTextBlock", metadata=utils.get_metadata("1.2")) - horizontal_alignment: Optional[ct.HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TextRun: - # pylint: disable=too-many-instance-attributes - """ - Represents a text run. - - Attributes: - text: The text content of the text run. - type: The type of the text run. - color: The color of the text run. - font_type: The font type of the text run. - highlight: Specifies whether the text run should be highlighted. - is_subtle: Specifies whether the text run is subtle. - italic: Specifies whether the text run is italicized. - select_action: The select action associated with the text run. - size: The font size of the text run. - strikethrough: Specifies whether the text run should have a strikethrough effect. - underline: Specifies whether the text run should be underlined. - weight: The font weight of the text run. - """ - - text: str = field(metadata=utils.get_metadata("1.2")) - type: str = field(default="TextRun", metadata=utils.get_metadata("1.2")) - color: Optional[ct.Colors] = field(default=None, metadata=utils.get_metadata("1.2")) - font_type: Optional[ct.FontType] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - highlight: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - is_subtle: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - italic: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - select_action: Optional[actions.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - size: Optional[ct.FontSize] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - strikethrough: Optional[bool] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - underline: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.3")) - weight: Optional[ct.FontWeight] = field( - default=None, metadata=utils.get_metadata("1.2") - ) diff --git a/adaptive_cards/inputs.py b/adaptive_cards/inputs.py deleted file mode 100644 index d3d7d3b..0000000 --- a/adaptive_cards/inputs.py +++ /dev/null @@ -1,250 +0,0 @@ -"""Implementations for adaptive card input types""" - -from dataclasses import dataclass, field -from typing import Union, Optional -from dataclasses_json import dataclass_json, LetterCase -from adaptive_cards import utils -import adaptive_cards.card_types as ct -from adaptive_cards import actions - -InputTypes = Union[ - "InputText", - "InputNumber", - "InputDate", - "InputTime", - "InputToggle", - "InputChoiceSet", -] - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Input: - # pylint: disable=too-many-instance-attributes - """ - Represents an input. - - Attributes: - error_message: The error message for the input. - is_required: Specifies whether the input is required. - label: The label of the input. - fallback: The fallback input. - height: The height of the input. - separator: Specifies whether a separator should be displayed before the input. - spacing: The spacing of the input. - is_visible: Specifies whether the input is visible. - requires: The requirements for the input. - """ - - error_message: Optional[str] = field( - default=None, metadata=utils.get_metadata("1.3") - ) - is_required: Optional[bool] = field( - default=None, metadata=utils.get_metadata("1.3") - ) - label: Optional[str] = field(default=None, metadata=utils.get_metadata("1.3")) - fallback: Optional[InputTypes] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - height: Optional[ct.BlockElementHeight] = field( - default=None, metadata=utils.get_metadata("1.1") - ) - separator: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.0")) - spacing: Optional[ct.Spacing] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - is_visible: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - requires: Optional[dict[str, str]] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputText(Input): - # pylint: disable=too-many-instance-attributes - """ - Represents a text input. - - Inherits from Input. - - Attributes: - id: The ID of the input. - type: The type of the input. - is_multiline: Specifies whether the input supports multiline text. - max_length: The maximum length of the input text. - placeholder: The placeholder text for the input. - regex: The regular expression pattern for validating the input text. - style: The style of the text input. - inline_action: The inline action associated with the input. - value: The initial value of the input. - """ - - id: str = field(metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - type: str = field(default="Input.Text", metadata=utils.get_metadata("1.0")) - is_multiline: Optional[bool] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - max_length: Optional[int] = field(default=None, metadata=utils.get_metadata("1.0")) - placeholder: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - regex: Optional[str] = field(default=None, metadata=utils.get_metadata("1.3")) - style: Optional[ct.TextInputStyle] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - inline_action: Optional[actions.SelectAction] = field( - default=None, metadata=utils.get_metadata("1.2") - ) - value: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputNumber(Input): - """ - Represents an input field for numerical values. - - Inherits from Input. - - Attributes: - id: The ID of the input. - type: The type of the input, which is "Input.Number". - max: The maximum value allowed for the input. Optional. - min: The minimum value allowed for the input. Optional. - placeholder: The placeholder text for the input. Optional. - value: The initial value of the input. Optional. - """ - - id: str = field(metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - type: str = field(default="Input.Number", metadata=utils.get_metadata("1.0")) - max: Optional[int] = field(default=None, metadata=utils.get_metadata("1.0")) - min: Optional[int] = field(default=None, metadata=utils.get_metadata("1.0")) - placeholder: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - value: Optional[int] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputDate(Input): - """ - Represents an input field for date values. - - Inherits from Input. - - Attributes: - id: The ID of the input. - type: The type of the input, which is "Input.Date". - max: The maximum date allowed for the input. Optional. - min: The minimum date allowed for the input. Optional. - placeholder: The placeholder text for the input. Optional. - value: The initial value of the input. Optional. - """ - - id: str = field(metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - type: str = field(default="Input.Date", metadata=utils.get_metadata("1.0")) - max: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - min: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - placeholder: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - value: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputTime(Input): - """ - Represents an input field for time values. - - Inherits from Input. - - Attributes: - id: The ID of the input. - type: The type of the input, which is "Input.Time". - max: The maximum time allowed for the input. Optional. - min: The minimum time allowed for the input. Optional. - placeholder: The placeholder text for the input. Optional. - value: The initial value of the input. Optional. - """ - - id: str = field(metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - type: str = field(default="Input.Time", metadata=utils.get_metadata("1.0")) - max: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - min: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - placeholder: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - value: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputToggle(Input): - """ - Represents a toggle input field. - - Inherits from Input. - - Attributes: - id: The ID of the input. - title: The title or label for the input. - type: The type of the input, which is "Input.Toggle". - value: The initial value of the input. Optional. - value_off: The value when the toggle is turned off. Optional. - value_on: The value when the toggle is turned on. Optional. - wrap: Indicates whether the input should wrap to the next line if needed. Optional. - """ - - id: str = field(metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - title: str = field(metadata=utils.get_metadata("1.0")) - type: str = field(default="Input.Toggle", metadata=utils.get_metadata("1.0")) - value: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - value_off: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - value_on: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - wrap: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputChoiceSet(Input): - # pylint: disable=too-many-instance-attributes - """ - Represents a choice set input field. - - Inherits from Input. - - Attributes: - id: The ID of the input. - type: The type of the input, which is "Input.ChoiceSet". - choices: The list of choices for the input. Optional. - is_multi_select: Indicates whether multiple choices can be selected. Optional. - style: The style of the choice input. Optional. - value: The initial value of the input. Optional. - placeholder: The placeholder text for the input. Optional. - wrap: Indicates whether the input should wrap to the next line if needed. Optional. - """ - - id: str = field(metadata=utils.get_metadata("1.0")) # pylint: disable=C0103 - type: str = field(default="Input.ChoiceSet", metadata=utils.get_metadata("1.0")) - choices: Optional[list["InputChoice"]] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - is_multi_select: Optional[bool] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - style: Optional[ct.ChoiceInputStyle] = field( - default=None, metadata=utils.get_metadata("1.0") - ) - value: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - placeholder: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - wrap: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.2")) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class InputChoice: - """ - Represents a choice within an input choice set. - - Attributes: - title: The title or display text of the choice. - value: The value associated with the choice. - """ - - title: str = field(metadata=utils.get_metadata("1.0")) - value: str = field(metadata=utils.get_metadata("1.0")) diff --git a/examples/simple_card/simple_card.py b/examples/simple_card/simple_card.py index f569699..f5087d4 100644 --- a/examples/simple_card/simple_card.py +++ b/examples/simple_card/simple_card.py @@ -6,7 +6,8 @@ from adaptive_cards.card import AdaptiveCard from adaptive_cards.client import TeamsClient from adaptive_cards.elements import TextBlock -from adaptive_cards.validation import CardValidator, CardValidatorFactory, Result +from adaptive_cards.validation import CardValidator, CardValidatorFactory +from result import Result, is_ok text_block: TextBlock = TextBlock( text="It's your second card", @@ -17,13 +18,13 @@ # build card version: str = "1.4" -card: AdaptiveCard = AdaptiveCard.new().version(version).add_item(text_block).create() +card: AdaptiveCard = AdaptiveCard.new().add_item(text_block).create() # validate card validator: CardValidator = CardValidatorFactory.create_validator_microsoft_teams() result: Result = validator.validate(card) -print(f"Validation was successful: {result == Result.SUCCESS}") +print(f"Validation was successful: {is_ok(result)}") # send card webhook_url: str = "YOUR-URL" diff --git a/examples/update_card/update_card.py b/examples/update_card/update_card.py new file mode 100644 index 0000000..733e160 --- /dev/null +++ b/examples/update_card/update_card.py @@ -0,0 +1,37 @@ +"""Example: simple card""" + +import adaptive_cards.card_types as types +from adaptive_cards.actions import ActionOpenUrl +from adaptive_cards.card import AdaptiveCard +from adaptive_cards.elements import TextBlock +from result import Result, is_ok + +text_block: TextBlock = TextBlock( + id="text-id", + text="Initial text", +) + +action_open_url: ActionOpenUrl = ActionOpenUrl( + id="action-id", url="any-url", title="title" +) + +# build card +version: str = "1.4" +card: AdaptiveCard = ( + AdaptiveCard.new().add_item(text_block).add_action(action_open_url).create() +) + +# update card item +update_result: Result[None, str] = card.update_item( + id="text-id", + text="New text", + horizontal_alignment=types.HorizontalAlignment.CENTER, + font_type=types.FontType.MONOSPACE, +) +assert is_ok(update_result) +print(card) + +# update card action +update_result = card.update_action(id="action-id", url="new-url") +assert is_ok(update_result) +print(card) diff --git a/examples/validate_card/validate_card.py b/examples/validate_card/validate_card.py new file mode 100644 index 0000000..81fa4da --- /dev/null +++ b/examples/validate_card/validate_card.py @@ -0,0 +1,28 @@ +from adaptive_cards.card import AdaptiveCard +from adaptive_cards.validation import CardValidatorFactory, CardValidator, Finding +from result import Result, is_ok +from adaptive_cards.elements import TextBlock +import adaptive_cards.card_types as types + +text_block: TextBlock = TextBlock( + text="It's your second card", + color=types.Colors.ACCENT, + size=types.FontSize.EXTRA_LARGE, + horizontal_alignment=types.HorizontalAlignment.CENTER, +) + +version: str = "1.4" +card: AdaptiveCard = ( + AdaptiveCard.new().version(version).add_items([text_block]).create() +) + +validator: CardValidator = CardValidatorFactory.create_validator_microsoft_teams() +result: Result[None, str] = validator.validate(card) + +print(f"Validation was successful: {is_ok(result)}") + +card_size: float = validator.card_size(card) +print(card_size) + +details: list[Finding] = validator.details() +assert len(details) == 0 diff --git a/examples/wrap_up_card/wrap_up_card.py b/examples/wrap_up_card/wrap_up_card.py index 70f5b66..c610642 100644 --- a/examples/wrap_up_card/wrap_up_card.py +++ b/examples/wrap_up_card/wrap_up_card.py @@ -13,7 +13,8 @@ from adaptive_cards.client import TeamsClient from adaptive_cards.containers import Column, ColumnSet, Container, ContainerTypes from adaptive_cards.elements import Image, TextBlock -from adaptive_cards.validation import CardValidator, CardValidatorFactory, Result +from adaptive_cards.validation import CardValidator, CardValidatorFactory +from result import Result, is_ok containers: list[ContainerTypes] = [] @@ -167,7 +168,7 @@ card_validator: CardValidator = CardValidatorFactory.create_validator_microsoft_teams() result: Result = card_validator.validate(card) -print(f"Validation was successful: {result == Result.SUCCESS}") +print(f"Validation was successful: {is_ok(result)}") # send card webhook_url: str = "YOUR-URL" diff --git a/pyproject.toml b/pyproject.toml index a7435fe..2f4ce99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,8 @@ # pyproject.toml -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - [project] name = "adaptive-cards-py" -version = "0.0.7" +version = "0.2.4" description = "Python wrapper library for building beautiful adaptive cards" classifiers = [ "License :: OSI Approved :: MIT License", @@ -16,7 +12,13 @@ classifiers = [ readme = "README.md" license = { file = "LICENSE" } keywords = ["bot", "ui", "adaptivecards", "cards", "adaptivecardsio", "python"] -dependencies = ["dataclasses-json", "requests", "jsonschema"] +dependencies = [ + "requests", + "jsonschema", + "pydantic>=2.10.6", + "mypy>=1.15.0", + "result>=0.17.0", +] requires-python = ">=3.10" [project.urls] @@ -24,3 +26,15 @@ Homepage = "https://github.com/dennis6p/adaptive-cards-py" [tool.setuptools.package-data] adaptive_cards = ["schemas/*.json"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=8.3.5", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/adaptive_cards"] diff --git a/ruff.toml b/ruff.toml index d4f5e09..6d15ad0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -33,14 +33,14 @@ line-length = 88 indent-width = 4 # Assume Python 3.9 -target-version = "py39" +target-version = "py310" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. -select = ["E4", "E7", "E9", "F"] -ignore = ["F403"] +select = ["C", "E", "F", "W", "B", "N"] +ignore = ["F403", "E501", "B024"] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] diff --git a/adaptive_cards/__init__.py b/src/adaptive_cards/__init__.py similarity index 53% rename from adaptive_cards/__init__.py rename to src/adaptive_cards/__init__.py index 68dcb78..b89664d 100644 --- a/adaptive_cards/__init__.py +++ b/src/adaptive_cards/__init__.py @@ -1,6 +1,6 @@ """ -This package provides small components for building adaptive cards compliant to -the current interface definite with Python. +This package provides small components for building adaptive cards compliant to +the current interface definite with Python. [Schema Explorer](https://adaptivecards.io/explorer/) @@ -9,16 +9,6 @@ By importing the package, the following modulesare directly available: - [actions, card_types, card, client, containers, elements, inputs, utils, validation] -This module also initializes the package and ensures that any necessary +This module also initializes the package and ensures that any necessary configuration or setup is performed. """ - -from adaptive_cards.actions import * -from adaptive_cards.card_types import * -from adaptive_cards.card import * -from adaptive_cards.client import * -from adaptive_cards.containers import * -from adaptive_cards.elements import * -from adaptive_cards.inputs import * -from adaptive_cards.utils import * -from adaptive_cards.validation import * diff --git a/src/adaptive_cards/actions.py b/src/adaptive_cards/actions.py new file mode 100644 index 0000000..5fe1bb9 --- /dev/null +++ b/src/adaptive_cards/actions.py @@ -0,0 +1,221 @@ +"""Implementations for all adaptive card action types""" + +from __future__ import annotations +from typing import Any, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + +import adaptive_cards.card_types as ct +from adaptive_cards import utils + + +class Action(BaseModel): + # pylint: disable=too-many-instance-attributes + """ + Represents an action that can be performed. + + Attributes: + title: An optional string representing the title of the action. + icon_url: An optional string representing the URL of the icon associated with the action. + id: An optional string representing the ID of the action. + style: An optional ActionStyle enum value representing the style of the action. + fallback: An optional fallback ActionTypes object representing the fallback action to be + performed. + tooltip: An optional string representing the tooltip text for the action. + is_enabled: An optional boolean indicating whether the action is enabled or disabled. + mode: An optional ActionMode enum value representing the mode of the action. + requires: An optional dictionary mapping string keys to string values representing the + requirements for the action. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + title: Optional[str] = Field( + default=None, + json_schema_extra=utils.get_metadata("1.0"), + ) + icon_url: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + id: Optional[str] = Field(default=None, json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + style: Optional[ct.ActionStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + fallback: Optional["ActionTypes"] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + tooltip: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + is_enabled: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + mode: Optional[ct.ActionMode] = Field( + default=ct.ActionMode.PRIMARY, json_schema_extra=utils.get_metadata("1.5") + ) + requires: Optional[dict[str, str]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + + +class ActionOpenUrl(Action): + """ + Represents an action to open a URL. + + Inherits from Action + + Attributes: + url: The URL to be opened. + type: The type of the action. Default is "Action.OpenUrl". + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + url: str = Field(json_schema_extra=utils.get_metadata("1.0")) + type: str = Field( + default="Action.OpenUrl", + json_schema_extra=utils.get_metadata("1.0"), + frozen=True, + ) + + +class ActionSubmit(Action): + """ + Represents an action to submit data. + + Inherits from Action. + + Attributes: + type: The type of the action. Default is "Action.Submit". + data: Optional data associated with the action. + associated_inputs: Optional associated inputs for the action. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="Action.Submit", + json_schema_extra=utils.get_metadata("1.0"), + frozen=True, + ) + data: Optional[str | Any] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + associated_inputs: Optional[ct.AssociatedInputs] = Field( + default=None, json_schema_extra=utils.get_metadata("1.3") + ) + + +class ActionShowCard(Action): + """ + Represents an action to show a card. + + Inherits from Action. + + Attributes: + type: The type of the action. Default is "Action.ShowCard". + card: Optional card to show. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="Action.ShowCard", + json_schema_extra=utils.get_metadata("1.0"), + frozen=True, + ) + card: Optional[Any] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class TargetElement(BaseModel): + """ + Represents a target element. + + Attributes: + element_id: The ID of the target element. + is_visible: Optional flag indicating the visibility of the target element. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + element_id: str = Field(json_schema_extra=utils.get_metadata("1.0")) + is_visible: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class ActionToggleVisibility(Action): + """ + Represents an action that toggles the visibility of target elements. + + Inherits from Action. + + Attributes: + target_elements: A list of TargetElement objects representing the target elements to toggle. + type: The type of the action, set to "Action.ToggleVisibility". + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + target_elements: list[TargetElement] = Field( + json_schema_extra=utils.get_metadata("1.2") + ) + type: str = Field( + default="Action.ToggleVisibility", + json_schema_extra=utils.get_metadata("1.2"), + frozen=True, + ) + + +class ActionExecute(Action): + """ + Represents an action that executes a command or performs an action. + + Inherits from Action. + + Attributes: + type: The type of the action, set to "Action.ShowCard". + verb: An optional string representing the verb of the action. + data: An optional string or Any type representing additional data associated + with the action. + associated_inputs: An optional AssociatedInputs object representing associated + inputs for the action. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="Action.ShowCard", + json_schema_extra=utils.get_metadata("1.4"), + frozen=True, + ) + verb: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + data: Optional[str | Any] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + associated_inputs: Optional[ct.AssociatedInputs] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + + +ActionTypes = Union[ + ActionOpenUrl, + ActionSubmit, + ActionShowCard, + ActionToggleVisibility, + ActionExecute, +] + +SelectAction = Union[ + ActionExecute, + ActionOpenUrl, + ActionSubmit, + ActionToggleVisibility, +] + +Action.model_rebuild() diff --git a/adaptive_cards/card.py b/src/adaptive_cards/card.py similarity index 58% rename from adaptive_cards/card.py rename to src/adaptive_cards/card.py index 396a7cc..98af6be 100644 --- a/adaptive_cards/card.py +++ b/src/adaptive_cards/card.py @@ -1,9 +1,9 @@ """Implementation of the adaptive card type""" -from dataclasses import dataclass, field -from typing import Any, Literal, Optional, Sequence - -from dataclasses_json import LetterCase, dataclass_json +from __future__ import annotations +from typing import Any, Literal, Optional, Sequence, get_origin, get_args +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr +from pydantic.alias_generators import to_camel import adaptive_cards.card_types as ct from adaptive_cards import utils @@ -11,6 +11,7 @@ from adaptive_cards.containers import ContainerTypes from adaptive_cards.elements import Element from adaptive_cards.inputs import InputTypes +from result import Ok, Err, Result SCHEMA: str = "http://adaptivecards.io/schemas/adaptive-card.json" TYPE: str = "AdaptiveCard" @@ -24,21 +25,19 @@ class AdaptiveCardBuilder: def __init__(self) -> None: self.__reset() - def __reset(self) -> None: - self.__card = AdaptiveCard() - - def type(self, _type: str) -> "AdaptiveCardBuilder": - """ - Set type of card + @staticmethod + def __get_id( + component: Element | ContainerTypes | InputTypes | ActionTypes, + ) -> str | None: + if not hasattr(component, "id"): + return - Args: - type (str): Type of card + return component.id # type: ignore -> safe as attribute is checked before - Returns: - AdaptiveCardBuilder: Builder object - """ - self.__card.type = _type - return self + def __reset(self) -> None: + self.__card = AdaptiveCard() + self.__items: dict[str, Element | ContainerTypes | InputTypes] = {} + self.__actions: dict[str, ActionTypes] = {} def version(self, version: CardVersion) -> "AdaptiveCardBuilder": """ @@ -83,10 +82,10 @@ def authentication( def select_action(self, select_action: SelectAction) -> "AdaptiveCardBuilder": """ - Set select_action element for card + Set select_action component for card Args: - select_action (SelectAction): Action element when selected + select_action (SelectAction): Action component when selected Returns: AdaptiveCardBuilder: Builder object @@ -127,7 +126,7 @@ def metadata(self, metadata: ct.Metadata) -> "AdaptiveCardBuilder": Set additional metadata for card Args: - metadata (ct.Metadata): Object with addtional metadata + metadata (ct.Metadata): Object with additional metadata Returns: AdaptiveCardBuilder: Builder object @@ -212,7 +211,7 @@ def schema(self, schema: str) -> "AdaptiveCardBuilder": Returns: AdaptiveCardBuilder: Builder object """ - self.__card.schema = schema + self.__card.schema_ = schema return self def width(self, width: ct.MSTeamsCardWidth) -> "AdaptiveCardBuilder": @@ -250,6 +249,11 @@ def add_item( if self.__card.body is None: self.__card.body = [] self.__card.body.append(item) + + # add if id field is set + if id := self.__get_id(item): + self.__items[id] = item + return self def add_items( @@ -284,6 +288,10 @@ def add_action(self, action: ActionTypes) -> "AdaptiveCardBuilder": if self.__card.actions is None: self.__card.actions = [] self.__card.actions.append(action) + + # add if id field is set + if id := self.__get_id(action): + self.__actions[id] = action return self def add_actions(self, actions: list[ActionTypes]) -> "AdaptiveCardBuilder": @@ -310,15 +318,19 @@ def create(self) -> "AdaptiveCard": Please note: This method must be called to get a actual card object from the card builder. Returns: - AdaptiveCard: Fully defined apdative card object + AdaptiveCard: Fully defined adaptive card object """ + card: AdaptiveCard = self.__card + card._items = self.__items + card._actions = self.__actions + return self.__card # pylint: disable=too-many-instance-attributes -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass -class AdaptiveCard: + + +class AdaptiveCard(BaseModel): """ Represents an Adaptive Card. @@ -328,7 +340,7 @@ class AdaptiveCard: schema: The schema of the Adaptive Card. refresh: The refresh settings for the card. authentication: The authentication settings for the card. - body: The list of card elements. + body: The list of card items. actions: The list of card actions. select_action: The select action for the card. fallback_text: The fallback text for the card. @@ -342,44 +354,60 @@ class AdaptiveCard: msteams: Set specific properties for MS Teams as the target framework """ - type: str = field(default=TYPE, metadata=utils.get_metadata("1.0")) - version: str = field(default=VERSION, metadata=utils.get_metadata("1.0")) - schema: str = field( - default=SCHEMA, metadata=utils.get_metadata("1.0", field_name="$schema") + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + _items: dict[str, Element | ContainerTypes | InputTypes] = PrivateAttr({}) + _actions: dict[str, ActionTypes] = PrivateAttr({}) + + type: str = Field( + default=TYPE, json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + version: str = Field(default=VERSION, json_schema_extra=utils.get_metadata("1.0")) + schema_: str = Field( + default=SCHEMA, + alias="$schema", + json_schema_extra=utils.get_metadata("1.0", field_name="$schema"), + ) + refresh: Optional[ct.Refresh] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + authentication: Optional[ct.Authentication] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") ) - refresh: Optional[ct.Refresh] = field( - default=None, metadata=utils.get_metadata("1.4") + body: Optional[list[Element | ContainerTypes | InputTypes]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) - authentication: Optional[ct.Authentication] = field( - default=None, metadata=utils.get_metadata("1.4") + actions: Optional[list[ActionTypes]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) - body: Optional[list[Element | ContainerTypes | InputTypes]] = field( - default=None, metadata=utils.get_metadata("1.0") + select_action: Optional[SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") ) - actions: Optional[list[ActionTypes]] = field( - default=None, metadata=utils.get_metadata("1.0") + fallback_text: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) - select_action: Optional[SelectAction] = field( - default=None, metadata=utils.get_metadata("1.1") + background_image: Optional[ct.BackgroundImage | str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) - fallback_text: Optional[str] = field( - default=None, metadata=utils.get_metadata("1.0") + metadata: Optional[ct.Metadata] = Field( + default=None, json_schema_extra=utils.get_metadata("1.6") ) - background_image: Optional[ct.BackgroundImage | str] = field( - default=None, metadata=utils.get_metadata("1.0") + min_height: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") ) - metadata: Optional[ct.Metadata] = field( - default=None, metadata=utils.get_metadata("1.6") + rtl: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") ) - min_height: Optional[str] = field(default=None, metadata=utils.get_metadata("1.2")) - rtl: Optional[bool] = field(default=None, metadata=utils.get_metadata("1.5")) - speak: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - lang: Optional[str] = field(default=None, metadata=utils.get_metadata("1.0")) - vertical_content_align: Optional[ct.VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.1") + speak: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) - msteams: Optional[ct.MSTeams] = field( - default=None, metadata=utils.get_metadata("1.0") + lang: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + vertical_content_align: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + msteams: Optional[ct.MSTeams] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) @staticmethod @@ -393,6 +421,95 @@ def new() -> AdaptiveCardBuilder: """ return AdaptiveCardBuilder() + @staticmethod + def __get_field_type_from_annotation(annotation: Any) -> Any: + """Determine a fields type from its annotation. + + Args: + annotation (Any): Field annotation + + Returns: + Any: Type of the field + """ + if not get_origin(annotation): + return annotation + return get_args(annotation)[0] + + @staticmethod + def __update( + components: dict[str, Element | ContainerTypes | InputTypes] + | dict[str, ActionTypes], + id: str, + **kwargs, + ) -> Result[None, str]: + """Update an item's or action's field value(s). + + Args: + id (str): id of component to be updated + + Returns: + Result[None, str]: Result of update procedure. None if successful, Error message otherwise. + """ + # if id is not found -> exit + if not components.get(id): + return Err("No component found for given ID") + + for field, value in kwargs.items(): + # if attribute is not present -> exit + if not hasattr(components[id], field): + return Err("Field not found in component") + + # if new value's type doesn't match the expected one -> exit + field_info = components[id].model_fields[field] + field_type: Any = AdaptiveCard.__get_field_type_from_annotation( + field_info.annotation + ) + if not isinstance(value, field_type): + return Err("Value type does not match expected field type") + + setattr(components[id], field, value) + return Ok(None) + + def update_action(self, id: str, **kwargs) -> Result[None, str]: + """Update an action's field value + + Note: + The update procedure can only succeed if the following criteria is fulfilled: + - ID of an component has been set when the card was created initially + - The property about to be updated must be part of the actual data model of the parent component + - The property's type must match the defined type in the parent data model + + Examples: + `card.update_action(id="action-id", title="new title")` + + Args: + id (str): id of action component to be updated + + Returns: + Result[None, str]: Result of update procedure. None if successful, error string otherwise. + """ + return AdaptiveCard.__update(self._actions, id, **kwargs) + + def update_item(self, id: str, **kwargs) -> Result[None, str]: + """Update an item's field value + + Note: + The update procedure can only succeed if the following criteria is fulfilled: + - ID of an component has been set when the card was created initially + - The property about to be updated must be part of the actual data model of the parent component + - The property's type must match the defined type in the parent data model + + Examples: + `card.update_item(id="action-id", text="new text")` + + Args: + id (str): id of action component to be updated + + Returns: + Result[None, str]: Result of update procedure. None if successful, error string otherwise. + """ + return AdaptiveCard.__update(self._items, id, **kwargs) + def to_json(self) -> str: """ Converts the full adaptive card schema into a json string. @@ -400,7 +517,7 @@ def to_json(self) -> str: Returns: str: Adaptive card schema as JSON string. """ - return self.to_json() + return self.model_dump_json(exclude_none=True, by_alias=True, warnings=False) def to_dict(self) -> dict[str, Any]: """ @@ -410,4 +527,7 @@ def to_dict(self) -> dict[str, Any]: str: Adaptive card schema as dictionary. """ - return self.to_dict() + return self.model_dump(exclude_none=True, by_alias=True, warnings=False) + + +AdaptiveCard.model_rebuild() diff --git a/adaptive_cards/card_types.py b/src/adaptive_cards/card_types.py similarity index 74% rename from adaptive_cards/card_types.py rename to src/adaptive_cards/card_types.py index a282a0f..13a31f0 100644 --- a/adaptive_cards/card_types.py +++ b/src/adaptive_cards/card_types.py @@ -1,10 +1,11 @@ """Implementations for all general card-related types and enums""" -from dataclasses import dataclass, field +from __future__ import annotations from enum import Enum from typing import Optional +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel -from dataclasses_json import LetterCase, dataclass_json from adaptive_cards import utils @@ -319,9 +320,7 @@ class ChoiceInputStyle(str, Enum): FILTERED = "filtered" -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class BackgroundImage: +class BackgroundImage(BaseModel): """ Represents the background image properties. @@ -332,21 +331,21 @@ class BackgroundImage: vertical_alignment: The vertical alignment of the image. """ - uri: str = field(metadata=utils.get_metadata("1.0")) - fill_mode: Optional[ImageFillMode] = field( - default=None, metadata=utils.get_metadata("1.2") + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + uri: str = Field(json_schema_extra=utils.get_metadata("1.0")) + fill_mode: Optional[ImageFillMode] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") ) - horizontal_alignment: Optional[HorizontalAlignment] = field( - default=None, metadata=utils.get_metadata("1.2") + horizontal_alignment: Optional[HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") ) - vertical_alignment: Optional[VerticalAlignment] = field( - default=None, metadata=utils.get_metadata("1.2") + vertical_alignment: Optional[VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") ) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Refresh: +class Refresh(BaseModel): """ Represents the refresh properties. @@ -356,16 +355,20 @@ class Refresh: user_ids: The list of user IDs associated with the refresh. """ - action: Optional[str] = field(default=None, metadata=utils.get_metadata("1.4")) - expires: Optional[str] = field(default=None, metadata=utils.get_metadata("1.6")) - user_ids: Optional[list[str]] = field( - default=None, metadata=utils.get_metadata("1.4") + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + action: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + expires: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.6") + ) + user_ids: Optional[list[str]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") ) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class TokenExchangeResource: +class TokenExchangeResource(BaseModel): """ Represents a token exchange resource. @@ -375,14 +378,14 @@ class TokenExchangeResource: provider_id: The provider ID associated with the resource. """ - id: str = field(default="", metadata=utils.get_metadata("1.4")) # pylint: disable=C0103 - uri: str = field(default="", metadata=utils.get_metadata("1.4")) - provider_id: str = field(default="", metadata=utils.get_metadata("1.4")) + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + id: str = Field(default="", json_schema_extra=utils.get_metadata("1.4")) # pylint: disable=C0103 + uri: str = Field(default="", json_schema_extra=utils.get_metadata("1.4")) + provider_id: str = Field(default="", json_schema_extra=utils.get_metadata("1.4")) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class AuthCardButtons: + +class AuthCardButtons(BaseModel): """ Represents buttons used in an authentication card. @@ -393,15 +396,19 @@ class AuthCardButtons: image: The image URL of the button. """ - type: str = field(default="", metadata=utils.get_metadata("1.4")) - value: str = field(default="", metadata=utils.get_metadata("1.4")) - title: Optional[str] = field(default=None, metadata=utils.get_metadata("1.4")) - image: Optional[str] = field(default=None, metadata=utils.get_metadata("1.4")) + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + type: str = Field(default="", json_schema_extra=utils.get_metadata("1.4")) + value: str = Field(default="", json_schema_extra=utils.get_metadata("1.4")) + title: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + image: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Authentication: + +class Authentication(BaseModel): """ Represents authentication properties. @@ -412,21 +419,23 @@ class Authentication: buttons: The authentication buttons. """ - text: Optional[str] = field(default=None, metadata=utils.get_metadata("1.4")) - connection_name: Optional[str] = field( - default=None, metadata=utils.get_metadata("1.4") + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + text: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") + ) + connection_name: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") ) - token_exchange_resource: Optional[TokenExchangeResource] = field( - default=None, metadata=utils.get_metadata("1.4") + token_exchange_resource: Optional[TokenExchangeResource] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") ) - buttons: Optional[AuthCardButtons] = field( - default=None, metadata=utils.get_metadata("1.4") + buttons: Optional[AuthCardButtons] = Field( + default=None, json_schema_extra=utils.get_metadata("1.4") ) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class Metadata: +class Metadata(BaseModel): """ Represents metadata properties. @@ -434,12 +443,14 @@ class Metadata: web_url: The web URL. """ - web_url: Optional[str] = field(default=None, metadata=utils.get_metadata("1.6")) + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + web_url: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.6") + ) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass(kw_only=True) -class MSTeams: +class MSTeams(BaseModel): """ Represents specific properties for MS Teams as the target framework. @@ -448,6 +459,8 @@ class MSTeams: when posted to MS Teams. Defaults to "None". """ - width: Optional[MSTeamsCardWidth] = field( - default=MSTeamsCardWidth.DEFAULT, metadata=utils.get_metadata("1.0") + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + width: Optional[MSTeamsCardWidth] = Field( + default=MSTeamsCardWidth.DEFAULT, json_schema_extra=utils.get_metadata("1.0") ) diff --git a/adaptive_cards/client.py b/src/adaptive_cards/client.py similarity index 100% rename from adaptive_cards/client.py rename to src/adaptive_cards/client.py diff --git a/src/adaptive_cards/containers.py b/src/adaptive_cards/containers.py new file mode 100644 index 0000000..4a4fec9 --- /dev/null +++ b/src/adaptive_cards/containers.py @@ -0,0 +1,426 @@ +"""Implementations for all adaptive card container types""" + +from __future__ import annotations +from typing import Optional, Union + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +import adaptive_cards.actions as action +import adaptive_cards.card_types as ct +from adaptive_cards import elements, inputs, utils + + +class ContainerBase(BaseModel): + """ + The ContainerBase class represents a base container for elements with various properties. + + Attributes: + fallback: The fallback element, if any, to be displayed when the container + cannot be rendered. + separator: Determines whether a separator should be shown above the container. + spacing: The spacing style to be applied within the container. + id: The unique identifier of the container. + is_visible: Determines whether the container is visible. + requires: A dictionary of requirements that must be satisfied for the container + to be displayed. + height: The height style to be applied to the container. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + fallback: Optional[elements.Element | action.ActionTypes | inputs.InputTypes] = ( + Field(default=None, json_schema_extra=utils.get_metadata("1.2")) + ) + separator: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + spacing: Optional[ct.Spacing] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + # pylint: disable=C0103 + id: Optional[str] = Field(default=None, json_schema_extra=utils.get_metadata("1.2")) + is_visible: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + requires: Optional[dict[str, str]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + height: Optional[ct.BlockElementHeight] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + + +class ActionSet(ContainerBase): + """Represents an action set, a container for a list of actions. + + Inherits from ContainerBase. + + Attributes: + actions: A list of actions in the action set. + type: The type of the action set. Defaults to "ActionSet". + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + actions: list[action.ActionTypes] = Field( + json_schema_extra=utils.get_metadata("1.2") + ) + type: str = Field( + default="ActionSet", json_schema_extra=utils.get_metadata("1.2"), frozen=True + ) + + +class Container(ContainerBase): + # pylint: disable=too-many-instance-attributes + """Represents a container for elements with various properties. + + Inherits from ContainerBase. + + Attributes: + items: A list of elements, sub-containers, or input elements contained within the container. + type: The type of the container. Defaults to "Container". + select_action: An optional select action associated with the container. + style: The style of the container. + vertical_content_alignment: The vertical alignment of the container's content. + bleed: Determines whether the container bleeds beyond its boundary. + background_image: The background image of the container. + min_height: The minimum height of the container. + rtl: Determines whether the container's content is displayed right-to-left. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + items: list[elements.Element | ContainerTypes | inputs.InputTypes] = Field( + json_schema_extra=utils.get_metadata("1.0") + ) + type: str = Field( + default="Container", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + select_action: Optional[action.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + style: Optional[ct.ContainerStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + vertical_content_alignment: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + bleed: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + background_image: Optional[ct.BackgroundImage | str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + min_height: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + rtl: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + + +class ColumnSet(ContainerBase): + """Represents a set of columns within a container. + + Inherits from ContainerBase. + + Attributes: + type: The type of the column set. Defaults to "ColumnSet". + columns: An optional list of Column objects within the column set. + select_action: An optional select action associated with the column set. + style: The style of the column set. + bleed: Determines whether the column set bleeds beyond its boundary. + min_height: The minimum height of the column set. + horizontal_alignment: The horizontal alignment of the column set. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="ColumnSet", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + columns: Optional[list["Column"]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + select_action: Optional[action.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + style: Optional[ct.ContainerStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + bleed: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + min_height: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + horizontal_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class Column(ContainerBase): + # pylint: disable=too-many-instance-attributes + """Represents a column within a container. + + Inherits from ContainerBase. + + Attributes: + type: The type of the column. Defaults to "Column". + items: An optional list of elements contained within the column. + background_image: The background image of the column. + bleed: Determines whether the column bleeds beyond its boundary. + min_height: The minimum height of the column. + rtl: Determines whether the column's content is displayed right-to-left. + separator: Determines whether a separator should be shown above the column. + spacing: The spacing style to be applied within the column. + select_action: An optional select action associated with the column. + style: The style of the column. + vertical_content_alignment: The vertical alignment of the column's content. + width: The width of the column. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="Column", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + items: Optional[list[elements.Element | ContainerTypes | inputs.Input]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + background_image: Optional[ct.BackgroundImage | str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + bleed: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + min_height: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + rtl: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + separator: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + spacing: Optional[ct.Spacing] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + select_action: Optional[action.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + style: Optional[ct.ContainerStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + vertical_content_alignment: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + width: Optional[str | int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class FactSet(ContainerBase): + """Represents a set of facts within a container. + + Inherits from ContainerBase. + + Attributes: + facts: A list of Fact objects within the fact set. + type: The type of the fact set. Defaults to "FactSet". + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + facts: list["Fact"] = Field(json_schema_extra=utils.get_metadata("1.0")) + type: str = Field( + default="FactSet", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + + +class Fact(BaseModel): + """Represents a fact. + + Attributes: + title: The title of the fact. + value: The value of the fact. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + title: str = Field(json_schema_extra=utils.get_metadata("1.0")) + value: str = Field(json_schema_extra=utils.get_metadata("1.0")) + + +class ImageSet(ContainerBase): + """Represents a set of images within a container. + + Inherits from ContainerBase. + + Attributes: + images: A list of Image objects within the image set. + type: The type of the image set. Defaults to "ImageSet". + image_size: The size of the images within the image set. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + images: list[elements.Image] = Field(json_schema_extra=utils.get_metadata("1.0")) + type: str = Field( + default="ImageSet", json_schema_extra=utils.get_metadata("1.2"), frozen=True + ) + image_size: Optional[ct.ImageSize] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class TableColumnDefinition(BaseModel): + """Represents a definition for a table column. + + Attributes: + horizontal_cell_content_alignment: The horizontal alignment of cell content. + vertical_cell_content_alignment: The vertical alignment of cell content. + width: The width of the table column. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + horizontal_cell_content_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + vertical_cell_content_alignment: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + width: Optional[str | int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + + +class TableRow(BaseModel): + """Represents a row within a table. + + Attributes: + cells: The cells within the table row. + horizontal_cell_content_alignment: The horizontal alignment of cell content. + vertical_cell_content_alignment: The vertical alignment of cell content. + style: The style of the table row. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="TableRow", json_schema_extra=utils.get_metadata("1.5"), frozen=True + ) + cells: Optional[list["TableCell"]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + horizontal_cell_content_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + vertical_cell_content_alignment: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + style: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + + +class Table(ContainerBase): + # pylint: disable=too-many-instance-attributes + """Represents a table within a container. + + Inherits from ContainerBase. + + Attributes: + type: The type of the table. Defaults to "Table". + columns: The column definitions of the table. + rows: The rows of the table. + first_row_as_header: Whether the first row should be treated as a header. + show_grid_lines: Whether to show grid lines in the table. + grid_style: The style of the table grid. + horizontal_cell_content_alignment: The horizontal alignment of cell content. + vertical_cell_content_alignment: The vertical alignment of cell content. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field(default="Table", json_schema_extra=utils.get_metadata("1.5")) + columns: Optional[list[TableColumnDefinition]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + rows: Optional[list[TableRow]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + first_row_as_header: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + show_grid_lines: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + grid_style: Optional[ct.ContainerStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + horizontal_cell_content_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + vertical_cell_content_alignment: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + + +class TableCell(BaseModel): + # pylint: disable=too-many-instance-attributes + """Represents a cell within a table. + + Attributes: + items: The elements within the cell. + select_action: The action to perform when the cell is selected. + style: The style of the cell. + vertical_content_alignment: The vertical alignment of cell content. + bleed: Whether the cell should bleed beyond its boundaries. + background_image: The background image of the cell. + min_height: The minimum height of the cell. + rtl: Whether the cell should be rendered in right-to-left direction. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="TableCell", json_schema_extra=utils.get_metadata("1.5"), frozen=True + ) + items: list[elements.Element] = Field(json_schema_extra=utils.get_metadata("1.5")) + select_action: Optional[action.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + style: Optional[ct.ContainerStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + vertical_content_alignment: Optional[ct.VerticalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + bleed: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + background_image: Optional[ct.BackgroundImage | str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + min_height: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + rtl: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + + +ContainerTypes = Union[ + ActionSet, + Container, + ColumnSet, + FactSet, + ImageSet, + Table, +] + +ContainerBase.model_rebuild() diff --git a/src/adaptive_cards/elements.py b/src/adaptive_cards/elements.py new file mode 100644 index 0000000..42270d2 --- /dev/null +++ b/src/adaptive_cards/elements.py @@ -0,0 +1,315 @@ +"""Implementations for adaptive card element types""" + +from __future__ import annotations +from typing import Union, Optional, Any + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + +from adaptive_cards import actions +from adaptive_cards import utils +import adaptive_cards.card_types as ct + + +class CardElement(BaseModel): + """ + Represents a card element. + + Attributes: + Element: The element of the card. + separator: Indicates whether a separator should be displayed before the element. + spacing: The spacing for the element. + id: The ID of the element. + is_visible: Indicates whether the element is visible. + requires: The requirements for the element. + height: The height of the element. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + element: Optional[Any | Element] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + separator: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + spacing: Optional[ct.Spacing] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + id: Optional[str] = Field(default=None, json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + is_visible: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + requires: Optional[dict[str, str]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + height: Optional[ct.BlockElementHeight] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + + +class TextBlock(CardElement): + # pylint: disable=too-many-instance-attributes + """ + Represents a text block card element. + + Inherits from CardElement. + + Attributes: + text: The text content of the text block. + type: The type of the card element. + color: The color of the text block. + font_type: The font type of the text block. + horizontal_alignment: The horizontal alignment of the text block. + is_subtle: Indicates whether the text block has subtle styling. + max_lines: The maximum number of lines to display for the text block. + size: The font size of the text block. + weight: The font weight of the text block. + wrap: Indicates whether the text should wrap within the text block. + style: The style of the text block. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + text: str = Field(json_schema_extra=utils.get_metadata("1.0")) + type: str = Field( + default="TextBlock", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + color: Optional[ct.Colors] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + font_type: Optional[ct.FontType] = Field( + default=None, + json_schema_extra=utils.get_metadata("1.2"), + ) + horizontal_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + is_subtle: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + max_lines: Optional[int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + size: Optional[ct.FontSize] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + weight: Optional[ct.FontWeight] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + wrap: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + style: Optional[ct.TextBlockStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.5") + ) + + +class Image(CardElement): + # pylint: disable=too-many-instance-attributes + """ + Represents an image card element. + + Inherits from CardElement. + + Attributes: + url: The URL of the image. + type: The type of the card element. + alt_text: The alternative text for the image. + background_color: The background color of the image. + horizontal_alignment: The horizontal alignment of the image. + select_action: The select action associated with the image. + size: The size of the image. + style: The style of the image. + width: The width of the image. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + url: str = Field(json_schema_extra=utils.get_metadata("1.0")) + type: str = Field( + default="Image", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + alt_text: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + background_color: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + horizontal_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + select_action: Optional[actions.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + size: Optional[ct.ImageSize] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + style: Optional[ct.ImageStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + width: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + + +class Media(CardElement): + """ + Represents a media card element. + + Inherits from CardElement. + + Attributes: + type: The type of the card element. + sources: The list of media sources. + poster: The poster image URL. + alt_text: The alternative text for the media. + caption_sources: The list of caption sources. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + type: str = Field( + default="Media", json_schema_extra=utils.get_metadata("1.1"), frozen=True + ) + sources: list["MediaSource"] = Field(json_schema_extra=utils.get_metadata("1.1")) + poster: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + alt_text: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + caption_sources: Optional[list["CaptionSource"]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.6") + ) + + +class MediaSource(BaseModel): + """ + Represents a media source. + + Attributes: + url: The URL of the media source. + mime_type: The MIME type of the media source. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + url: str = Field(json_schema_extra=utils.get_metadata("1.1")) + mime_type: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + + +class CaptionSource(BaseModel): + """ + Represents a caption source. + + Attributes: + mime_type: The MIME type of the caption source. + url: The URL of the caption source. + label: The label of the caption source. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + mime_type: str = Field(json_schema_extra=utils.get_metadata("1.6")) + url: str = Field(json_schema_extra=utils.get_metadata("1.6")) + label: str = Field(json_schema_extra=utils.get_metadata("1.6")) + + +class RichTextBlock(CardElement): + """ + Represents a rich text block. + + Inherits from CardElement. + + Attributes: + inlines: A list of inlines in the rich text block. Each inline can be a string + or a TextRun object. + type: The type of the rich text block. + horizontal_alignment: The horizontal alignment of the rich text block. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + inlines: list[Union[str, "TextRun"]] = Field( + json_schema_extra=utils.get_metadata("1.2") + ) + type: str = Field( + default="RichTextBlock", + json_schema_extra=utils.get_metadata("1.2"), + frozen=True, + ) + horizontal_alignment: Optional[ct.HorizontalAlignment] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + + +class TextRun(BaseModel): + # pylint: disable=too-many-instance-attributes + """ + Represents a text run. + + Attributes: + text: The text content of the text run. + type: The type of the text run. + color: The color of the text run. + font_type: The font type of the text run. + highlight: Specifies whether the text run should be highlighted. + is_subtle: Specifies whether the text run is subtle. + italic: Specifies whether the text run is italicized. + select_action: The select action associated with the text run. + size: The font size of the text run. + strikethrough: Specifies whether the text run should have a strikethrough effect. + underline: Specifies whether the text run should be underlined. + weight: The font weight of the text run. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + text: str = Field(json_schema_extra=utils.get_metadata("1.2")) + type: str = Field( + default="TextRun", json_schema_extra=utils.get_metadata("1.2"), frozen=True + ) + color: Optional[ct.Colors] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + font_type: Optional[ct.FontType] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + highlight: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + is_subtle: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + italic: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + select_action: Optional[actions.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + size: Optional[ct.FontSize] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + strikethrough: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + underline: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.3") + ) + weight: Optional[ct.FontWeight] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + + +Element = Union[ + Image, + TextBlock, + Media, + CaptionSource, + RichTextBlock, +] + +CardElement.model_rebuild() diff --git a/src/adaptive_cards/inputs.py b/src/adaptive_cards/inputs.py new file mode 100644 index 0000000..a1e0389 --- /dev/null +++ b/src/adaptive_cards/inputs.py @@ -0,0 +1,316 @@ +"""Implementations for adaptive card input types""" + +from __future__ import annotations +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Union, Optional +from adaptive_cards import utils +import adaptive_cards.card_types as ct +from adaptive_cards import actions + + +class Input(BaseModel): + # pylint: disable=too-many-instance-attributes + """ + Represents an input. + + Attributes: + error_message: The error message for the input. + is_required: Specifies whether the input is required. + label: The label of the input. + fallback: The fallback input. + height: The height of the input. + separator: Specifies whether a separator should be displayed before the input. + spacing: The spacing of the input. + is_visible: Specifies whether the input is visible. + requires: The requirements for the input. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + error_message: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.3") + ) + is_required: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.3") + ) + label: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.3") + ) + fallback: Optional[InputTypes] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + height: Optional[ct.BlockElementHeight] = Field( + default=None, json_schema_extra=utils.get_metadata("1.1") + ) + separator: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + spacing: Optional[ct.Spacing] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + is_visible: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + requires: Optional[dict[str, str]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + + +class InputText(Input): + # pylint: disable=too-many-instance-attributes + """ + Represents a text input. + + Inherits from Input. + + Attributes: + id: The ID of the input. + type: The type of the input. + is_multiline: Specifies whether the input supports multiline text. + max_length: The maximum length of the input text. + placeholder: The placeholder text for the input. + regex: The regular expression pattern for validating the input text. + style: The style of the text input. + inline_action: The inline action associated with the input. + value: The initial value of the input. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str = Field(json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + type: str = Field( + default="Input.Text", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + is_multiline: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + max_length: Optional[int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + placeholder: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + regex: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.3") + ) + style: Optional[ct.TextInputStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + inline_action: Optional[actions.SelectAction] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + value: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class InputNumber(Input): + """ + Represents an input field for numerical values. + + Inherits from Input. + + Attributes: + id: The ID of the input. + type: The type of the input, which is "Input.Number". + max: The maximum value allowed for the input. Optional. + min: The minimum value allowed for the input. Optional. + placeholder: The placeholder text for the input. Optional. + value: The initial value of the input. Optional. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str = Field(json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + type: str = Field( + default="Input.Number", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + max: Optional[int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + min: Optional[int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + placeholder: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + value: Optional[int] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class InputDate(Input): + """ + Represents an input field for date values. + + Inherits from Input. + + Attributes: + id: The ID of the input. + type: The type of the input, which is "Input.Date". + max: The maximum date allowed for the input. Optional. + placeholder: The placeholder text for the input. Optional. + value: The initial value of the input. Optional. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str = Field(json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + type: str = Field( + default="Input.Date", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + max: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + placeholder: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + value: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class InputTime(Input): + """ + Represents an input field for time values. + + Inherits from Input. + + Attributes: + id: The ID of the input. + type: The type of the input, which is "Input.Time". + max: The maximum time allowed for the input. Optional. + min: The minimum time allowed for the input. Optional. + placeholder: The placeholder text for the input. Optional. + value: The initial value of the input. Optional. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str = Field(json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + type: str = Field( + default="Input.Time", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + max: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + min: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + placeholder: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + value: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + + +class InputToggle(Input): + """ + Represents a toggle input field. + + Inherits from Input. + + Attributes: + id: The ID of the input. + title: The title or label for the input. + type: The type of the input, which is "Input.Toggle". + value: The initial value of the input. Optional. + value_off: The value when the toggle is turned off. Optional. + value_on: The value when the toggle is turned on. Optional. + wrap: Indicates whether the input should wrap to the next line if needed. Optional. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str = Field(json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + title: str = Field(json_schema_extra=utils.get_metadata("1.0")) + type: str = Field( + default="Input.Toggle", json_schema_extra=utils.get_metadata("1.0"), frozen=True + ) + value: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + value_off: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + value_on: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + wrap: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + + +class InputChoiceSet(Input): + # pylint: disable=too-many-instance-attributes + """ + Represents a choice set input field. + + Inherits from Input. + + Attributes: + id: The ID of the input. + type: The type of the input, which is "Input.ChoiceSet". + choices: The list of choices for the input. Optional. + is_multi_select: Indicates whether multiple choices can be selected. Optional. + style: The style of the choice input. Optional. + value: The initial value of the input. Optional. + placeholder: The placeholder text for the input. Optional. + wrap: Indicates whether the input should wrap to the next line if needed. Optional. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str = Field(json_schema_extra=utils.get_metadata("1.0")) # pylint: disable=C0103 + type: str = Field( + default="Input.ChoiceSet", + json_schema_extra=utils.get_metadata("1.0"), + frozen=True, + ) + choices: Optional[list["InputChoice"]] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + is_multi_select: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + style: Optional[ct.ChoiceInputStyle] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + value: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + placeholder: Optional[str] = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") + ) + wrap: Optional[bool] = Field( + default=None, json_schema_extra=utils.get_metadata("1.2") + ) + + +class InputChoice(BaseModel): + """ + Represents a choice within an input choice set. + + Attributes: + title: The title or display text of the choice. + value: The value associated with the choice. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + title: str = Field(json_schema_extra=utils.get_metadata("1.0")) + value: str = Field(json_schema_extra=utils.get_metadata("1.0")) + + +InputTypes = Union[ + InputText, + InputNumber, + InputDate, + InputTime, + InputToggle, + InputChoiceSet, +] + +Input.model_rebuild() diff --git a/adaptive_cards/schemas/schema-1.0.json b/src/adaptive_cards/schemas/schema-1.0.json similarity index 100% rename from adaptive_cards/schemas/schema-1.0.json rename to src/adaptive_cards/schemas/schema-1.0.json diff --git a/adaptive_cards/schemas/schema-1.1.json b/src/adaptive_cards/schemas/schema-1.1.json similarity index 100% rename from adaptive_cards/schemas/schema-1.1.json rename to src/adaptive_cards/schemas/schema-1.1.json diff --git a/adaptive_cards/schemas/schema-1.2.json b/src/adaptive_cards/schemas/schema-1.2.json similarity index 100% rename from adaptive_cards/schemas/schema-1.2.json rename to src/adaptive_cards/schemas/schema-1.2.json diff --git a/adaptive_cards/schemas/schema-1.3.json b/src/adaptive_cards/schemas/schema-1.3.json similarity index 100% rename from adaptive_cards/schemas/schema-1.3.json rename to src/adaptive_cards/schemas/schema-1.3.json diff --git a/adaptive_cards/schemas/schema-1.4.json b/src/adaptive_cards/schemas/schema-1.4.json similarity index 100% rename from adaptive_cards/schemas/schema-1.4.json rename to src/adaptive_cards/schemas/schema-1.4.json diff --git a/adaptive_cards/schemas/schema-1.5.json b/src/adaptive_cards/schemas/schema-1.5.json similarity index 100% rename from adaptive_cards/schemas/schema-1.5.json rename to src/adaptive_cards/schemas/schema-1.5.json diff --git a/adaptive_cards/schemas/schema-1.6.json b/src/adaptive_cards/schemas/schema-1.6.json similarity index 100% rename from adaptive_cards/schemas/schema-1.6.json rename to src/adaptive_cards/schemas/schema-1.6.json diff --git a/adaptive_cards/utils.py b/src/adaptive_cards/utils.py similarity index 86% rename from adaptive_cards/utils.py rename to src/adaptive_cards/utils.py index 3a31c38..df5be58 100644 --- a/adaptive_cards/utils.py +++ b/src/adaptive_cards/utils.py @@ -4,8 +4,6 @@ from typing import Any -from dataclasses_json import config - def is_none(item: Any) -> bool: """ @@ -31,4 +29,4 @@ def get_metadata(min_version: str, field_name: str | None = None) -> dict[str, A Returns: dict[str, Any]: Metadata information """ - return config(exclude=is_none, field_name=field_name) | {"min_version": min_version} + return {"exclude": is_none, "field_name": field_name} | {"min_version": min_version} diff --git a/adaptive_cards/validation.py b/src/adaptive_cards/validation.py similarity index 90% rename from adaptive_cards/validation.py rename to src/adaptive_cards/validation.py index 614c4cd..b58272f 100644 --- a/adaptive_cards/validation.py +++ b/src/adaptive_cards/validation.py @@ -1,35 +1,23 @@ """Validation class for evaluating a cards schema""" -import dataclasses import json from abc import ABC, abstractmethod -from dataclasses import dataclass, fields -from enum import Enum, Flag +from enum import Enum from pathlib import Path from typing import Any, Literal +from pydantic import BaseModel -from jsonschema.exceptions import ValidationError +from jsonschema.exceptions import ValidationError as SchemaValidationError from jsonschema.validators import Draft6Validator from adaptive_cards.card import AdaptiveCard +from result import Result, Err, Ok MINIMUM_VERSION_KEY: str = "min_version" SchemaVersion = Literal["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"] -class Result(Flag): - """ - Represents the overall validation result value as a combination of flags. - """ - - SUCCESS = 0 - """validation successful""" - - FAILURE = 1 - """validation failed""" - - class ValidationFailure(str, Enum): """Validation failure types""" @@ -45,6 +33,13 @@ class ValidationFailure(str, Enum): SIZE_LIMIT_EXCEEDED = "card exceeds the allowed card size for framework" """size of card exceeds the defined limit for the target framework""" + ID_NOT_FOUND = "ID not found - requested element is not part of the card in scope" + """ID was not found for any element in card""" + + INVALID_VALUE_TYPE = "Value type does not match the field type meant to be updated" + """Value type does not match what is expected for the field as per definition in the + pydantic model""" + class AbstractTargetFramework(ABC): """ @@ -177,9 +172,6 @@ def __init__(self): class CardValidatorAbstractFactory(ABC): """Abstract card validator factory""" - def __init__(self): - pass - @classmethod @abstractmethod def create_validator_bot(cls) -> "AbstractCardValidator": @@ -300,8 +292,7 @@ def create_validator_windows_widgets(cls) -> "CardValidator": return CardValidator(WindowsWidgets()) -@dataclass -class Finding: +class Finding(BaseModel): """ Class for storing an individual finding during schema validation """ @@ -315,17 +306,26 @@ class Finding: message_additional: str = "" """Addition information""" + def __init__( + self, + failure: ValidationFailure, + message: str, + message_additional: str = "", + ) -> None: + super(Finding, self).__init__( + failure=failure, + message=message, + message_additional=message_additional, + ) + class AbstractCardValidator(ABC): """ Abstract interface for card validators """ - def __init__(self): - pass - @abstractmethod - def validate(self, card: AdaptiveCard, debug: bool = True) -> Result: + def validate(self, card: AdaptiveCard, debug: bool = True) -> Result[None, Err]: """ Run validation on card. @@ -333,7 +333,7 @@ def validate(self, card: AdaptiveCard, debug: bool = True) -> Result: card (AdaptiveCard): Card to be validated Returns: - Result: Validation result + Result[None, Err]: Result of validation """ @classmethod @@ -375,7 +375,7 @@ def __init__(self, target_framework: AbstractTargetFramework) -> None: self.__findings: list[Finding] self.__card_size: float - def validate(self, card: AdaptiveCard, debug: bool = True) -> Result: + def validate(self, card: AdaptiveCard, debug: bool = True) -> Result[None, str]: self.__card = card self.__reset() self.__validate_card() @@ -383,7 +383,7 @@ def validate(self, card: AdaptiveCard, debug: bool = True) -> Result: if debug: self.__debug() - return Result.SUCCESS if len(self.__findings) == 0 else Result.FAILURE + return Ok(None) if len(self.__findings) == 0 else Err("Validation failed") @classmethod def card_size(cls, card: AdaptiveCard) -> float: @@ -454,8 +454,11 @@ def __validate_version_for_elements(self, items: Any | list[Any]): for item in items: self.__item = item - for field in fields(item): - value: Any = getattr(item, field.name) + + for field in item.model_fields_set: + field_name: str = field.__str__() + + value: Any = getattr(item, field_name) if value is None: continue @@ -464,12 +467,13 @@ def __validate_version_for_elements(self, items: Any | list[Any]): iterables.append(value) continue - if dataclasses.is_dataclass(value): + if isinstance(value, BaseModel): custom_types.append(value) continue + metadata = item.model_fields[field_name].json_schema_extra self.__validate_field_version( - field.name, field.metadata.get(MINIMUM_VERSION_KEY) + field_name, metadata.get(MINIMUM_VERSION_KEY) ) for iterable in iterables: @@ -490,8 +494,7 @@ def __validate_card_size(self) -> None: Finding( ValidationFailure.SIZE_LIMIT_EXCEEDED, ValidationFailure.SIZE_LIMIT_EXCEEDED.value, - f"{self.__target_framework.name()} | " - f"{self.__target_framework.max_card_size()} KB", + f"{self.__target_framework.name()} | {self.__target_framework.max_card_size()} KB", ) ) @@ -521,9 +524,9 @@ def __read_schema_file(self) -> dict[str, Any]: def __validate_schema(self) -> None: schema: dict[str, Any] = self.__read_schema_file() try: - Draft6Validator(schema).validate(json.loads(self.__card.to_json())) + Draft6Validator(schema).validate(instance=self.__card.to_dict()) - except ValidationError as ex: + except SchemaValidationError as ex: self.__findings.append( Finding( ValidationFailure.INVALID_SCHEMA, diff --git a/tests/test_card.py b/tests/test_card.py new file mode 100644 index 0000000..dfa5b53 --- /dev/null +++ b/tests/test_card.py @@ -0,0 +1,122 @@ +from adaptive_cards.card import AdaptiveCard +from adaptive_cards.inputs import InputText +from result import is_ok, is_err +from adaptive_cards.actions import ActionSubmit + + +class TestAdaptiveCard: + def test_create_card(self): + card = AdaptiveCard.new().version("1.2").create() + assert card.type == "AdaptiveCard" + assert card.version == "1.2" + + def test_add_item(self): + input_text: InputText = InputText(id="test", label="test") + card: AdaptiveCard = AdaptiveCard.new().add_item(input_text).create() + assert card.body is not None + assert input_text in card.body + + def test_update_item_success(self): + input_text: InputText = InputText(id="id", label="test") + card = AdaptiveCard.new().add_item(input_text).version("1.3").create() + + input_text_updated: InputText = InputText(id="id", label="updated") + card_updated = ( + AdaptiveCard.new().add_item(input_text_updated).version("1.3").create() + ) + + result = card.update_item("id", label="updated") + + assert is_ok(result) + assert card_updated == card + + def test_update_item_not_found(self): + input_text: InputText = InputText(id="id", label="test") + card = AdaptiveCard.new().add_item(input_text).version("1.3").create() + + input_text_updated: InputText = InputText(id="id", label="updated") + card_updated = ( + AdaptiveCard.new().add_item(input_text_updated).version("1.3").create() + ) + + result = card.update_item("wrong-id", label="updated") + + assert is_err(result) + assert card_updated != card + + def test_update_item_field_not_found(self): + input_text: InputText = InputText(id="id", label="test") + card = AdaptiveCard.new().add_item(input_text).version("1.3").create() + + input_text_updated: InputText = InputText(id="id", label="updated") + card_updated = ( + AdaptiveCard.new().add_item(input_text_updated).version("1.3").create() + ) + + result = card.update_item("id", wrong_field="updated") + + assert is_err(result) + assert card_updated != card + + def test_update_item_wrong_value_type(self): + input_text: InputText = InputText(id="id", label="test") + card = AdaptiveCard.new().add_item(input_text).version("1.3").create() + + input_text_updated: InputText = InputText(id="id", label="updated") + card_updated = ( + AdaptiveCard.new().add_item(input_text_updated).version("1.3").create() + ) + + result = card.update_item("id", label=1) + + assert is_err(result) + assert card_updated != card + + def test_add_action(self): + action_submit: ActionSubmit = ActionSubmit(id="action1", title="Submit") + card: AdaptiveCard = AdaptiveCard.new().add_action(action_submit).create() + assert card.actions is not None + assert action_submit in card.actions + + def test_add_actions(self): + action_submit1: ActionSubmit = ActionSubmit(id="action1", title="Submit1") + action_submit2: ActionSubmit = ActionSubmit(id="action2", title="Submit2") + card: AdaptiveCard = ( + AdaptiveCard.new().add_actions([action_submit1, action_submit2]).create() + ) + assert card.actions is not None + assert action_submit1 in card.actions + assert action_submit2 in card.actions + + def test_update_action_success(self): + action_submit: ActionSubmit = ActionSubmit(id="action1", title="Submit") + card = AdaptiveCard.new().add_action(action_submit).version("1.3").create() + + result = card.update_action("action1", title="Updated Submit") + + assert is_ok(result) + assert card._actions["action1"].title == "Updated Submit" + + def test_update_action_element_not_found(self): + action_submit: ActionSubmit = ActionSubmit(id="action1", title="Submit") + card = AdaptiveCard.new().add_action(action_submit).version("1.3").create() + + result = card.update_action("wrong-id", title="Updated Submit") + + assert is_err(result) + + def test_update_action_field_not_found(self): + action_submit: ActionSubmit = ActionSubmit(id="action1", title="Submit") + card = AdaptiveCard.new().add_action(action_submit).version("1.3").create() + + result = card.update_action("action1", wrong_field="Updated Submit") + + assert is_err(result) + + def test_update_action_wrong_value_type(self): + action_submit: ActionSubmit = ActionSubmit(id="action1", title="Submit") + card = AdaptiveCard.new().add_action(action_submit).version("1.3").create() + + result = card.update_action("action1", title=123) + + assert is_err(result) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..027e1ad --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,63 @@ +import pytest +from unittest.mock import patch, Mock +from http import HTTPStatus +from adaptive_cards.card import AdaptiveCard +from adaptive_cards.client import TeamsClient +from adaptive_cards.elements import TextBlock + +# filepath: src/adaptive_cards/test_client.py + + +class TestTeamsClient: + @patch("adaptive_cards.client.requests.post") + def test_send_single_card_success(self, mock_post): + mock_response = Mock() + mock_response.status_code = HTTPStatus.OK + mock_post.return_value = mock_response + + client = TeamsClient("http://example.com/webhook") + card = AdaptiveCard.new().add_item(TextBlock(text="Test Card")).create() + response = client.send(card) + + assert response.status_code == HTTPStatus.OK + mock_post.assert_called_once() + + @patch("adaptive_cards.client.requests.post") + def test_send_multiple_cards_success(self, mock_post): + mock_response = Mock() + mock_response.status_code = HTTPStatus.ACCEPTED + mock_post.return_value = mock_response + + client = TeamsClient("http://example.com/webhook") + card1 = AdaptiveCard.new().add_item(TextBlock(text="Test Card")).create() + card2 = AdaptiveCard.new().add_item(TextBlock(text="Test Card")).create() + response = client.send(card1, card2) + + assert response.status_code == HTTPStatus.ACCEPTED + mock_post.assert_called_once() + + def test_send_no_webhook_url(self): + client = TeamsClient("") + card = AdaptiveCard.new().add_item(TextBlock(text="Test Card")).create() + + with pytest.raises(ValueError, match="No webhook URL provided."): + client.send(card) + + def test_send_no_cards_provided(self): + client = TeamsClient("http://example.com/webhook") + + with pytest.raises(ValueError, match="No cards provided."): + client.send() + + @patch("adaptive_cards.client.requests.post") + def test_send_failed_request(self, mock_post): + mock_response = Mock() + mock_response.status_code = HTTPStatus.BAD_REQUEST + mock_response.text = "Bad Request" + mock_post.return_value = mock_response + + client = TeamsClient("http://example.com/webhook") + card = AdaptiveCard.new().add_item(TextBlock(text="Test Card")).create() + + with pytest.raises(RuntimeError, match="Failed to send card: 400, Bad Request"): + client.send(card) diff --git a/tests/test_validation.py b/tests/test_validation.py index 97e558e..57a0d4b 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,28 +1,20 @@ """Tests for validation module""" -import unittest -from dataclasses import dataclass, field - -from dataclasses_json import dataclass_json - -import adaptive_cards.card_types as types -from adaptive_cards import ( - AdaptiveCard, - TextBlock, - utils, -) +from adaptive_cards.card import AdaptiveCard from adaptive_cards.validation import ( CardValidator, CardValidatorFactory, - Result, ValidationFailure, ) +from adaptive_cards.elements import TextBlock +from adaptive_cards.card_types import FontType +from adaptive_cards import utils +from result import is_ok, is_err +from pydantic import BaseModel, Field -class TestAdaptiveCardValidation(unittest.TestCase): - """Test class for Adaptive Card validaiton""" - - def test_card_validator_ms_teams_validate_success(self) -> None: +class TestCardValidation: + def test_card_validator_ms_teams_validate_success(self): """Test validation for ms teams""" validator: CardValidator = ( CardValidatorFactory.create_validator_microsoft_teams() @@ -30,61 +22,58 @@ def test_card_validator_ms_teams_validate_success(self) -> None: card: AdaptiveCard = ( AdaptiveCard.new().add_item(TextBlock(text="Test Card")).create() ) - self.assertEqual(validator.validate(card), Result.SUCCESS) - self.assertEqual(len(validator.details()), 0) + assert is_ok(validator.validate(card)) + assert len(validator.details()) == 0 - def test_validate_failure_empty_body(self) -> None: + def test_validate_failure_empty_body(self): """Test validation for ms teams""" validator: CardValidator = ( CardValidatorFactory.create_validator_microsoft_teams() ) card: AdaptiveCard = AdaptiveCard.new().create() - self.assertEqual(validator.validate(card), Result.FAILURE) - self.assertEqual(len(validator.details()), 1) - self.assertEqual(validator.details()[0].failure, ValidationFailure.EMPTY_CARD) + assert is_err(validator.validate(card)) + assert len(validator.details()) == 1 + assert validator.details()[0].failure == ValidationFailure.EMPTY_CARD - def test_validate_failure_invalid_field_version(self) -> None: + def test_validate_failure_invalid_field_version(self): """Test validation for ms teams""" validator: CardValidator = ( CardValidatorFactory.create_validator_microsoft_teams() ) + + # should fail as font_type is available only card schemas > 1.2 + text_block = TextBlock(text="Test Card", font_type=FontType.MONOSPACE) card: AdaptiveCard = ( - AdaptiveCard.new() - .version("1.0") - .add_item(TextBlock(text="Test Card", font_type=types.FontType.MONOSPACE)) - .create() - ) - self.assertEqual(validator.validate(card), Result.FAILURE) - self.assertEqual(len(validator.details()), 1) - self.assertEqual( - validator.details()[0].failure, ValidationFailure.INVALID_FIELD_VERSION + AdaptiveCard.new().version("1.0").add_item(text_block).create() ) + assert is_err(validator.validate(card, debug=True)) + assert len(validator.details()) == 1 + assert validator.details()[0].failure == ValidationFailure.INVALID_FIELD_VERSION - def test_validate_failure_invalid_schema(self) -> None: + def test_validate_failure_invalid_schema(self): """Test validation for ms teams""" validator: CardValidator = ( CardValidatorFactory.create_validator_microsoft_teams() ) - @dataclass_json - @dataclass - class InvalidClass: + class InvalidClass(BaseModel): """Invalid class""" - some_field: int | None = field( - default=None, metadata=utils.get_metadata("1.0") + some_field: int | None = Field( + default=None, json_schema_extra=utils.get_metadata("1.0") ) card: AdaptiveCard = ( - AdaptiveCard.new().version("1.0").add_item(InvalidClass(1)).create() # - ) - self.assertEqual(validator.validate(card), Result.FAILURE) - self.assertEqual(len(validator.details()), 1) - self.assertEqual( - validator.details()[0].failure, ValidationFailure.INVALID_SCHEMA + AdaptiveCard.new() + .version("1.0") + .add_item(InvalidClass(some_field=1)) # type: ignore + .create() ) + assert is_err(validator.validate(card)) + assert len(validator.details()) == 1 + assert validator.details()[0].failure == ValidationFailure.INVALID_SCHEMA - def test_validate_failure_size_limit_exceeded(self) -> None: + def test_validate_failure_size_limit_exceeded(self): """Test validation for ms teams""" validator: CardValidator = ( CardValidatorFactory.create_validator_microsoft_teams() @@ -93,26 +82,20 @@ def test_validate_failure_size_limit_exceeded(self) -> None: card: AdaptiveCard = ( AdaptiveCard.new() .version("1.0") - .add_items([TextBlock(text="TestCard") for i in range(660)]) - .create() # + .add_items([TextBlock(text="TestCard") for i in range(730)]) + .create() ) - self.assertTrue(validator.card_size(card) < 28) - self.assertEqual(validator.validate(card), Result.SUCCESS) + assert validator.card_size(card) < 28 + assert is_ok(validator.validate(card)) card = ( AdaptiveCard.new() .version("1.0") - .add_items([TextBlock(text="TestCard") for i in range(670)]) - .create() # - ) - self.assertTrue(validator.card_size(card) > 28) - self.assertEqual(validator.validate(card), Result.FAILURE) - self.assertEqual(len(validator.details()), 1) - self.assertEqual( - validator.details()[0].failure, ValidationFailure.SIZE_LIMIT_EXCEEDED + .add_items([TextBlock(text="TestCard") for i in range(740)]) + .create() ) - - -if __name__ == "__main__": - unittest.main() + assert validator.card_size(card) > 28 + assert is_err(validator.validate(card)) + assert len(validator.details()) == 1 + assert validator.details()[0].failure == ValidationFailure.SIZE_LIMIT_EXCEEDED diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..50b6fd6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,534 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "adaptive-cards-py" +version = "0.2.4" +source = { editable = "." } +dependencies = [ + { name = "jsonschema" }, + { name = "mypy" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "result" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "jsonschema" }, + { name = "mypy", specifier = ">=1.15.0" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "requests" }, + { name = "result", specifier = ">=0.17.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "result" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/47/2175be65744aa4d8419c27bd3a7a7d65af5bcad7a4dc6a812c00778754f0/result-0.17.0.tar.gz", hash = "sha256:b73da420c0cb1a3bf741dbd41ff96dedafaad6a1b3ef437a9e33e380bb0d91cf", size = 20180 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/90/19110ce9374c3db619e2df0816f2c58e4ddc5cdad5f7284cd81d8b30b7cb/result-0.17.0-py3-none-any.whl", hash = "sha256:49fd668b4951ad15800b8ccefd98b6b94effc789607e19c65064b775570933e8", size = 11689 }, +] + +[[package]] +name = "rpds-py" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/79/2ce611b18c4fd83d9e3aecb5cba93e1917c050f556db39842889fa69b79f/rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707", size = 26806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/fe/e5326459863bd525122f4e9c80ac8d7c6cfa171b7518d04cc27c12c209b0/rpds_py-0.23.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2a54027554ce9b129fc3d633c92fa33b30de9f08bc61b32c053dc9b537266fed", size = 372123 }, + { url = "https://files.pythonhosted.org/packages/f9/db/f10a3795f7a89fb27594934012d21c61019bbeb516c5bdcfbbe9e9e617a7/rpds_py-0.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5ef909a37e9738d146519657a1aab4584018746a18f71c692f2f22168ece40c", size = 356778 }, + { url = "https://files.pythonhosted.org/packages/21/27/0d3678ad7f432fa86f8fac5f5fc6496a4d2da85682a710d605219be20063/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee9d6f0b38efb22ad94c3b68ffebe4c47865cdf4b17f6806d6c674e1feb4246", size = 385775 }, + { url = "https://files.pythonhosted.org/packages/99/a0/1786defa125b2ad228027f22dff26312ce7d1fee3c7c3c2682f403db2062/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7356a6da0562190558c4fcc14f0281db191cdf4cb96e7604c06acfcee96df15", size = 391181 }, + { url = "https://files.pythonhosted.org/packages/f1/5c/1240934050a7ffd020a915486d0cc4c7f6e7a2442a77aedf13664db55d36/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9441af1d25aed96901f97ad83d5c3e35e6cd21a25ca5e4916c82d7dd0490a4fa", size = 444607 }, + { url = "https://files.pythonhosted.org/packages/b7/1b/cee6905b47817fd0a377716dbe4df35295de46df46ee2ff704538cc371b0/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d8abf7896a91fb97e7977d1aadfcc2c80415d6dc2f1d0fca5b8d0df247248f3", size = 445550 }, + { url = "https://files.pythonhosted.org/packages/54/f7/f0821ca34032892d7a67fcd5042f50074ff2de64e771e10df01085c88d47/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b08027489ba8fedde72ddd233a5ea411b85a6ed78175f40285bd401bde7466d", size = 386148 }, + { url = "https://files.pythonhosted.org/packages/eb/ef/2afe53bc857c4bcba336acfd2629883a5746e7291023e017ac7fc98d85aa/rpds_py-0.23.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fee513135b5a58f3bb6d89e48326cd5aa308e4bcdf2f7d59f67c861ada482bf8", size = 416780 }, + { url = "https://files.pythonhosted.org/packages/ae/9a/38d2236cf669789b8a3e1a014c9b6a8d7b8925b952c92e7839ae2749f9ac/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35d5631ce0af26318dba0ae0ac941c534453e42f569011585cb323b7774502a5", size = 558265 }, + { url = "https://files.pythonhosted.org/packages/e6/0a/f2705530c42578f20ed0b5b90135eecb30eef6e2ba73e7ba69087fad2dba/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a20cb698c4a59c534c6701b1c24a968ff2768b18ea2991f886bd8985ce17a89f", size = 585270 }, + { url = "https://files.pythonhosted.org/packages/29/4e/3b597dc84ed82c3d757ac9aa620de224a94e06d2e102069795ae7e81c015/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e9c206a1abc27e0588cf8b7c8246e51f1a16a103734f7750830a1ccb63f557a", size = 553850 }, + { url = "https://files.pythonhosted.org/packages/00/cc/6498b6f79e4375e6737247661e52a2d18f6accf4910e0c8da978674b4241/rpds_py-0.23.1-cp310-cp310-win32.whl", hash = "sha256:d9f75a06ecc68f159d5d7603b734e1ff6daa9497a929150f794013aa9f6e3f12", size = 220660 }, + { url = "https://files.pythonhosted.org/packages/17/2b/08db023d23e8c7032c99d8d2a70d32e450a868ab73d16e3ff5290308a665/rpds_py-0.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:f35eff113ad430b5272bbfc18ba111c66ff525828f24898b4e146eb479a2cdda", size = 232551 }, + { url = "https://files.pythonhosted.org/packages/1c/67/6e5d4234bb9dee062ffca2a5f3c7cd38716317d6760ec235b175eed4de2c/rpds_py-0.23.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b79f5ced71efd70414a9a80bbbfaa7160da307723166f09b69773153bf17c590", size = 372264 }, + { url = "https://files.pythonhosted.org/packages/a7/0a/3dedb2daee8e783622427f5064e2d112751d8276ee73aa5409f000a132f4/rpds_py-0.23.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9e799dac1ffbe7b10c1fd42fe4cd51371a549c6e108249bde9cd1200e8f59b4", size = 356883 }, + { url = "https://files.pythonhosted.org/packages/ed/fc/e1acef44f9c24b05fe5434b235f165a63a52959ac655e3f7a55726cee1a4/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721f9c4011b443b6e84505fc00cc7aadc9d1743f1c988e4c89353e19c4a968ee", size = 385624 }, + { url = "https://files.pythonhosted.org/packages/97/0a/a05951f6465d01622720c03ef6ef31adfbe865653e05ed7c45837492f25e/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f88626e3f5e57432e6191cd0c5d6d6b319b635e70b40be2ffba713053e5147dd", size = 391500 }, + { url = "https://files.pythonhosted.org/packages/ea/2e/cca0583ec0690ea441dceae23c0673b99755710ea22f40bccf1e78f41481/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:285019078537949cecd0190f3690a0b0125ff743d6a53dfeb7a4e6787af154f5", size = 444869 }, + { url = "https://files.pythonhosted.org/packages/cc/e6/95cda68b33a6d814d1e96b0e406d231ed16629101460d1740e92f03365e6/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92f5654157de1379c509b15acec9d12ecf6e3bc1996571b6cb82a4302060447", size = 444930 }, + { url = "https://files.pythonhosted.org/packages/5f/a7/e94cdb73411ae9c11414d3c7c9a6ad75d22ad4a8d094fb45a345ba9e3018/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e768267cbe051dd8d1c5305ba690bb153204a09bf2e3de3ae530de955f5b5580", size = 386254 }, + { url = "https://files.pythonhosted.org/packages/dd/c5/a4a943d90a39e85efd1e04b1ad5129936786f9a9aa27bb7be8fc5d9d50c9/rpds_py-0.23.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5334a71f7dc1160382d45997e29f2637c02f8a26af41073189d79b95d3321f1", size = 417090 }, + { url = "https://files.pythonhosted.org/packages/0c/a0/80d0013b12428d1fce0ab4e71829400b0a32caec12733c79e6109f843342/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6adb81564af0cd428910f83fa7da46ce9ad47c56c0b22b50872bc4515d91966", size = 557639 }, + { url = "https://files.pythonhosted.org/packages/a6/92/ec2e6980afb964a2cd7a99cbdef1f6c01116abe94b42cbe336ac93dd11c2/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cafa48f2133d4daa028473ede7d81cd1b9f9e6925e9e4003ebdf77010ee02f35", size = 584572 }, + { url = "https://files.pythonhosted.org/packages/3d/ce/75b6054db34a390789a82523790717b27c1bd735e453abb429a87c4f0f26/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fced9fd4a07a1ded1bac7e961ddd9753dd5d8b755ba8e05acba54a21f5f1522", size = 553028 }, + { url = "https://files.pythonhosted.org/packages/cc/24/f45abe0418c06a5cba0f846e967aa27bac765acd927aabd857c21319b8cc/rpds_py-0.23.1-cp311-cp311-win32.whl", hash = "sha256:243241c95174b5fb7204c04595852fe3943cc41f47aa14c3828bc18cd9d3b2d6", size = 220862 }, + { url = "https://files.pythonhosted.org/packages/2d/a6/3c0880e8bbfc36451ef30dc416266f6d2934705e468db5d21c8ba0ab6400/rpds_py-0.23.1-cp311-cp311-win_amd64.whl", hash = "sha256:11dd60b2ffddba85715d8a66bb39b95ddbe389ad2cfcf42c833f1bcde0878eaf", size = 232953 }, + { url = "https://files.pythonhosted.org/packages/f3/8c/d17efccb9f5b9137ddea706664aebae694384ae1d5997c0202093e37185a/rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c", size = 364369 }, + { url = "https://files.pythonhosted.org/packages/6e/c0/ab030f696b5c573107115a88d8d73d80f03309e60952b64c584c70c659af/rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba", size = 349965 }, + { url = "https://files.pythonhosted.org/packages/b3/55/b40170f5a079c4fb0b6a82b299689e66e744edca3c3375a8b160fb797660/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31", size = 389064 }, + { url = "https://files.pythonhosted.org/packages/ab/1c/b03a912c59ec7c1e16b26e587b9dfa8ddff3b07851e781e8c46e908a365a/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149", size = 397741 }, + { url = "https://files.pythonhosted.org/packages/52/6f/151b90792b62fb6f87099bcc9044c626881fdd54e31bf98541f830b15cea/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c", size = 448784 }, + { url = "https://files.pythonhosted.org/packages/71/2a/6de67c0c97ec7857e0e9e5cd7c52405af931b303eb1e5b9eff6c50fd9a2e/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5", size = 440203 }, + { url = "https://files.pythonhosted.org/packages/db/5e/e759cd1c276d98a4b1f464b17a9bf66c65d29f8f85754e27e1467feaa7c3/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc", size = 391611 }, + { url = "https://files.pythonhosted.org/packages/1c/1e/2900358efcc0d9408c7289769cba4c0974d9db314aa884028ed7f7364f61/rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35", size = 423306 }, + { url = "https://files.pythonhosted.org/packages/23/07/6c177e6d059f5d39689352d6c69a926ee4805ffdb6f06203570234d3d8f7/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b", size = 562323 }, + { url = "https://files.pythonhosted.org/packages/70/e4/f9097fd1c02b516fff9850792161eb9fc20a2fd54762f3c69eae0bdb67cb/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef", size = 588351 }, + { url = "https://files.pythonhosted.org/packages/87/39/5db3c6f326bfbe4576ae2af6435bd7555867d20ae690c786ff33659f293b/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad", size = 557252 }, + { url = "https://files.pythonhosted.org/packages/fd/14/2d5ad292f144fa79bafb78d2eb5b8a3a91c358b6065443cb9c49b5d1fedf/rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057", size = 222181 }, + { url = "https://files.pythonhosted.org/packages/a3/4f/0fce63e0f5cdd658e71e21abd17ac1bc9312741ebb8b3f74eeed2ebdf771/rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165", size = 237426 }, + { url = "https://files.pythonhosted.org/packages/13/9d/b8b2c0edffb0bed15be17b6d5ab06216f2f47f9ee49259c7e96a3ad4ca42/rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935", size = 363672 }, + { url = "https://files.pythonhosted.org/packages/bd/c2/5056fa29e6894144d7ba4c938b9b0445f75836b87d2dd00ed4999dc45a8c/rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4", size = 349602 }, + { url = "https://files.pythonhosted.org/packages/b0/bc/33779a1bb0ee32d8d706b173825aab75c628521d23ce72a7c1e6a6852f86/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6", size = 388746 }, + { url = "https://files.pythonhosted.org/packages/62/0b/71db3e36b7780a619698ec82a9c87ab44ad7ca7f5480913e8a59ff76f050/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10", size = 397076 }, + { url = "https://files.pythonhosted.org/packages/bb/2e/494398f613edf77ba10a916b1ddea2acce42ab0e3b62e2c70ffc0757ce00/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122", size = 448399 }, + { url = "https://files.pythonhosted.org/packages/dd/53/4bd7f5779b1f463243ee5fdc83da04dd58a08f86e639dbffa7a35f969a84/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4", size = 439764 }, + { url = "https://files.pythonhosted.org/packages/f6/55/b3c18c04a460d951bf8e91f2abf46ce5b6426fb69784166a6a25827cb90a/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013", size = 390662 }, + { url = "https://files.pythonhosted.org/packages/2a/65/cc463044a3cbd616029b2aa87a651cdee8288d2fdd7780b2244845e934c1/rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64", size = 422680 }, + { url = "https://files.pythonhosted.org/packages/fa/8e/1fa52990c7836d72e8d70cd7753f2362c72fbb0a49c1462e8c60e7176d0b/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8", size = 561792 }, + { url = "https://files.pythonhosted.org/packages/57/b8/fe3b612979b1a29d0c77f8585903d8b3a292604b26d4b300e228b8ac6360/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957", size = 588127 }, + { url = "https://files.pythonhosted.org/packages/44/2d/fde474de516bbc4b9b230f43c98e7f8acc5da7fc50ceed8e7af27553d346/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93", size = 556981 }, + { url = "https://files.pythonhosted.org/packages/18/57/767deeb27b81370bbab8f74ef6e68d26c4ea99018f3c71a570e506fede85/rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd", size = 221936 }, + { url = "https://files.pythonhosted.org/packages/7d/6c/3474cfdd3cafe243f97ab8474ea8949236eb2a1a341ca55e75ce00cd03da/rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70", size = 237145 }, + { url = "https://files.pythonhosted.org/packages/ec/77/e985064c624230f61efa0423759bb066da56ebe40c654f8b5ba225bd5d63/rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731", size = 359623 }, + { url = "https://files.pythonhosted.org/packages/62/d9/a33dcbf62b29e40559e012d525bae7d516757cf042cc9234bd34ca4b6aeb/rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5", size = 345900 }, + { url = "https://files.pythonhosted.org/packages/92/eb/f81a4be6397861adb2cb868bb6a28a33292c2dcac567d1dc575226055e55/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a", size = 386426 }, + { url = "https://files.pythonhosted.org/packages/09/47/1f810c9b5e83be005341201b5389f1d240dfa440346ea7189f9b3fd6961d/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e", size = 392314 }, + { url = "https://files.pythonhosted.org/packages/83/bd/bc95831432fd6c46ed8001f01af26de0763a059d6d7e6d69e3c5bf02917a/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f", size = 447706 }, + { url = "https://files.pythonhosted.org/packages/19/3e/567c04c226b1802dc6dc82cad3d53e1fa0a773258571c74ac5d8fbde97ed/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219", size = 437060 }, + { url = "https://files.pythonhosted.org/packages/fe/77/a77d2c6afe27ae7d0d55fc32f6841502648070dc8d549fcc1e6d47ff8975/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722", size = 389347 }, + { url = "https://files.pythonhosted.org/packages/3f/47/6b256ff20a74cfebeac790ab05586e0ac91f88e331125d4740a6c86fc26f/rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e", size = 415554 }, + { url = "https://files.pythonhosted.org/packages/fc/29/d4572469a245bc9fc81e35166dca19fc5298d5c43e1a6dd64bf145045193/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6", size = 557418 }, + { url = "https://files.pythonhosted.org/packages/9c/0a/68cf7228895b1a3f6f39f51b15830e62456795e61193d2c8b87fd48c60db/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b", size = 583033 }, + { url = "https://files.pythonhosted.org/packages/14/18/017ab41dcd6649ad5db7d00155b4c212b31ab05bd857d5ba73a1617984eb/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5", size = 554880 }, + { url = "https://files.pythonhosted.org/packages/2e/dd/17de89431268da8819d8d51ce67beac28d9b22fccf437bc5d6d2bcd1acdb/rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7", size = 219743 }, + { url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415 }, + { url = "https://files.pythonhosted.org/packages/95/a9/6fafd35fc6bac05f59bcbc800b57cef877911ff1c015397c519fec888642/rpds_py-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1f8afa346ccd59e4e5630d5abb67aba6a9812fddf764fd7eb11f382a345f8cc", size = 373463 }, + { url = "https://files.pythonhosted.org/packages/5b/ac/44f00029b8fbe0903a19e9a87a9b86063bf8700df2cc58868373d378418c/rpds_py-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fad784a31869747df4ac968a351e070c06ca377549e4ace94775aaa3ab33ee06", size = 358400 }, + { url = "https://files.pythonhosted.org/packages/5e/9c/3da199346c68d785f10dccab123b74c8c5f73be3f742c9e33d1116e07931/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5a96fcac2f18e5a0a23a75cd27ce2656c66c11c127b0318e508aab436b77428", size = 386815 }, + { url = "https://files.pythonhosted.org/packages/d3/45/8f6533c33c0d33da8c2c8b2fb8f2ee90b23c05c679b86b0ac6aee4653749/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e77febf227a1dc3220159355dba68faa13f8dca9335d97504abf428469fb18b", size = 392974 }, + { url = "https://files.pythonhosted.org/packages/ca/56/6a9ac1bf0455ba07385d8fe98c571c519b4f2000cff6581487bf9fab9272/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26bb3e8de93443d55e2e748e9fd87deb5f8075ca7bc0502cfc8be8687d69a2ec", size = 446019 }, + { url = "https://files.pythonhosted.org/packages/f4/83/5d9a3f9731cdccf49088bcc4ce821a5cf50bd1737cdad83e9959a7b9054d/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db7707dde9143a67b8812c7e66aeb2d843fe33cc8e374170f4d2c50bd8f2472d", size = 445811 }, + { url = "https://files.pythonhosted.org/packages/44/50/f2e0a98c62fc1fe68b176caca587714dc5c8bb2c3d1dd1eeb2bd4cc787ac/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eedaaccc9bb66581d4ae7c50e15856e335e57ef2734dbc5fd8ba3e2a4ab3cb6", size = 388070 }, + { url = "https://files.pythonhosted.org/packages/f2/d0/4981878f8f157e6dbea01d95e0119bf3d6b4c2c884fe64a9e6987f941104/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28358c54fffadf0ae893f6c1050e8f8853e45df22483b7fff2f6ab6152f5d8bf", size = 419173 }, + { url = "https://files.pythonhosted.org/packages/ce/13/fc971c470da96b270d2f64fedee987351bd935dc3016932a5cdcb1a88a2a/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:633462ef7e61d839171bf206551d5ab42b30b71cac8f10a64a662536e057fdef", size = 559048 }, + { url = "https://files.pythonhosted.org/packages/42/02/be91e1de139ec8b4f9fec4192fd779ba48af281cfc762c0ca4c15b945484/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a98f510d86f689fcb486dc59e6e363af04151e5260ad1bdddb5625c10f1e95f8", size = 584773 }, + { url = "https://files.pythonhosted.org/packages/27/28/3af8a1956df3edc41d884267d766dc096496dafc83f02f764a475eca0b4a/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e0397dd0b3955c61ef9b22838144aa4bef6f0796ba5cc8edfc64d468b93798b4", size = 555153 }, + { url = "https://files.pythonhosted.org/packages/5e/bb/e45f51c4e1327dea3c72b846c6de129eebacb7a6cb309af7af35d0578c80/rpds_py-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b", size = 233827 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +]