From 8ee4876a4223a238561a70af3c980d905e628983 Mon Sep 17 00:00:00 2001 From: Armin Graf Date: Sun, 18 Jan 2026 23:07:34 -0500 Subject: [PATCH 1/3] refactor(sdk): rewrite SDK for ServerlessWorkflow 1.0 specification Complete rewrite of the Python SDK to conform to the Serverless Workflow specification v1.0, replacing the previous v0.8 implementation. - Replace WorkflowValidator with automatic parsing validation - `base.py`: New base types (Duration, Error, Timeout, Input/Output, etc.) - `authentication.py`: OAuth2, OIDC, Bearer, Basic, Digest auth policies - `call_tasks.py`: HTTP, gRPC, OpenAPI, AsyncAPI, MCP function calls - `tasks.py`: Core task types (Do, Fork, For, Listen, Emit, Set, Switch, Try, Wait, Run, Raise) - `events.py`: Event consumption strategies and correlation - `retry.py`: Retry policies with backoff strategies - `endpoint.py`: Endpoint and catalog definitions - `draw.py`: Workflow visualization using Graphviz (930 lines) - `workflow.py`: Rewritten Workflow class with Document, Schedule, Use components - Replace Pipfile/pipenv with pyproject.toml/uv - Add pre-commit hooks configuration - Add specification submodule for validation testing - Update to Python 3.10+ - Configure ruff for linting/formatting, mypy for type checking - Modernize GitHub Actions workflow - Split into separate lint, test, build jobs - Add ruff linting and formatting checks - Add mypy type checking - Test matrix: Python 3.10, 3.11, 3.12 - Use uv for faster dependency management - Remove old v0.8 test examples (13 JSON files) - Add comprehensive v1.0 spec validation tests (1,748 lines) - Add visualization tests with 74 DOT fixtures - Test against official specification examples via submodule - Update README for v1.0 API with modern examples - Replace state-based examples with task-based patterns - Update installation instructions for uv - Document workflow visualization capabilities - Reference new test locations ## Breaking Changes - Complete API rewrite - no backwards compatibility with v0.8 - All state classes removed - WorkflowValidator removed (validation now automatic) - StateMachineHelper replaced with workflow.render_graph() Signed-off-by: Armin Graf --- .github/workflows/python-ci.yml | 68 +- .gitignore | 3 + .gitmodules | 3 + .pre-commit-config.yaml | 157 ++ Pipfile | 18 - Pipfile.lock | 345 ---- README.md | 252 +-- pyproject.toml | 172 ++ serverlessworkflow/__init__.py | 1 + serverlessworkflow/sdk/__init__.py | 245 +++ serverlessworkflow/sdk/action.py | 57 - serverlessworkflow/sdk/action_data_filter.py | 16 - serverlessworkflow/sdk/auth_def.py | 37 - serverlessworkflow/sdk/authentication.py | 167 ++ serverlessworkflow/sdk/base.py | 170 ++ serverlessworkflow/sdk/basic_props_def.py | 15 - serverlessworkflow/sdk/bearer_props_def.py | 13 - serverlessworkflow/sdk/branch.py | 30 - serverlessworkflow/sdk/branch_timeout.py | 12 - serverlessworkflow/sdk/call_tasks.py | 340 ++++ serverlessworkflow/sdk/callback_state.py | 79 - .../sdk/callback_state_timeout.py | 24 - serverlessworkflow/sdk/continue_as_def.py | 32 - serverlessworkflow/sdk/correlation_def.py | 12 - serverlessworkflow/sdk/cron_def.py | 12 - .../sdk/databased_switch_state.py | 74 - .../sdk/databased_switch_state_timeout.py | 20 - .../sdk/default_condition_def.py | 31 - serverlessworkflow/sdk/draw.py | 930 +++++++++ serverlessworkflow/sdk/end.py | 37 - serverlessworkflow/sdk/end_data_condition.py | 29 - serverlessworkflow/sdk/end_event_condition.py | 37 - serverlessworkflow/sdk/endpoint.py | 33 + serverlessworkflow/sdk/error.py | 39 - serverlessworkflow/sdk/error_def.py | 14 - .../sdk/event_based_switch_state.py | 74 - .../sdk/event_based_switch_state_timeout.py | 22 - serverlessworkflow/sdk/event_data_filter.py | 14 - serverlessworkflow/sdk/event_def.py | 43 - serverlessworkflow/sdk/event_ref.py | 34 - serverlessworkflow/sdk/event_state.py | 72 - serverlessworkflow/sdk/event_state_timeout.py | 24 - serverlessworkflow/sdk/events.py | 72 + serverlessworkflow/sdk/foreach_state.py | 85 - .../sdk/foreach_state_timeout.py | 22 - serverlessworkflow/sdk/function.py | 21 - serverlessworkflow/sdk/function_ref.py | 25 - serverlessworkflow/sdk/inject_state.py | 63 - .../sdk/inject_state_timeout.py | 20 - serverlessworkflow/sdk/metadata.py | 10 - serverlessworkflow/sdk/oauth2props_def.py | 33 - serverlessworkflow/sdk/on_events.py | 32 - serverlessworkflow/sdk/operation_state.py | 74 - .../sdk/operation_state_timeout.py | 22 - serverlessworkflow/sdk/parallel_state.py | 82 - .../sdk/parallel_state_timeout.py | 22 - serverlessworkflow/sdk/produce_event_def.py | 30 - serverlessworkflow/sdk/retry.py | 62 + serverlessworkflow/sdk/retry_def.py | 24 - serverlessworkflow/sdk/schedule.py | 27 - serverlessworkflow/sdk/serializable.py | 1 - serverlessworkflow/sdk/sleep.py | 12 - serverlessworkflow/sdk/sleep_state.py | 68 - serverlessworkflow/sdk/sleep_state_timeout.py | 16 - serverlessworkflow/sdk/start_def.py | 25 - serverlessworkflow/sdk/state.py | 34 - serverlessworkflow/sdk/state_data_filter.py | 12 - serverlessworkflow/sdk/state_exec_timeout.py | 12 - .../sdk/state_machine_extensions.py | 41 - .../sdk/state_machine_generator.py | 619 ------ .../sdk/state_machine_helper.py | 111 -- serverlessworkflow/sdk/sub_flow_ref.py | 16 - serverlessworkflow/sdk/swf_base.py | 175 -- serverlessworkflow/sdk/tasks.py | 378 ++++ serverlessworkflow/sdk/transition.py | 26 - .../sdk/transition_data_condition.py | 30 - .../sdk/transition_event_condition.py | 37 - serverlessworkflow/sdk/workflow.py | 669 +++++-- .../sdk/workflow_exec_timeout.py | 14 - serverlessworkflow/sdk/workflow_time_out.py | 33 - serverlessworkflow/sdk/workflow_validator.py | 25 - setup.py | 20 - submodules/specification | 1 + tests/__init__.py | 1 + tests/examples/advertise-listing.json | 57 - tests/examples/applicantrequest.json | 59 - tests/examples/booklending.json | 138 -- tests/examples/carauctionbids.json | 49 - tests/examples/checkcarvitals.json | 60 - tests/examples/graph.json | 181 -- tests/examples/helloworld.json | 18 - tests/examples/jobmonitoring.json | 127 -- tests/examples/parallel.json | 34 - tests/examples/provisionorder.json | 100 - tests/examples/second-subgraph.json | 66 - tests/examples/sendcloudevent.json | 47 - tests/examples/solvemathproblems.json | 37 - tests/serverlessworkflow/__init__.py | 0 tests/serverlessworkflow/sdk/__init__.py | 0 tests/serverlessworkflow/sdk/test_action.py | 36 - .../sdk/test_event_based_switch_state.py | 16 - tests/serverlessworkflow/sdk/test_hydrate.py | 34 - .../serverlessworkflow/sdk/test_workflow.json | 34 - tests/serverlessworkflow/sdk/test_workflow.py | 155 -- .../serverlessworkflow/sdk/test_workflow.yaml | 20 - .../sdk/test_workflow_validator.py | 38 - tests/specification/test_spec_examples.py | 1748 +++++++++++++++++ .../test_spec_examples_validations.py | 31 + tests/visualization/__init__.py | 1 + .../fixtures/accumulate-room-readings.dot | 23 + .../authentication-bearer-uri-format.dot | 14 + .../fixtures/authentication-bearer.dot | 12 + .../fixtures/authentication-oauth2-secret.dot | 14 + .../fixtures/authentication-oauth2.dot | 14 + .../fixtures/authentication-oidc-secret.dot | 14 + .../fixtures/authentication-oidc.dot | 14 + .../fixtures/authentication-reusable.dot | 14 + .../fixtures/call-asyncapi-publish.dot | 12 + ...call-asyncapi-subscribe-consume-amount.dot | 12 + ...capi-subscribe-consume-forever-foreach.dot | 18 + .../call-asyncapi-subscribe-consume-until.dot | 12 + .../call-asyncapi-subscribe-consume-while.dot | 12 + .../call-custom-function-cataloged.dot | 12 + .../fixtures/call-custom-function-inline.dot | 12 + tests/visualization/fixtures/call-grpc.dot | 12 + ...-http-endpoint-interpolation-shorthand.dot | 14 + .../call-http-endpoint-interpolation.dot | 12 + .../call-http-query-headers-expressions.dot | 14 + .../fixtures/call-http-query-parameters.dot | 12 + .../fixtures/call-http-redirect.dot | 12 + tests/visualization/fixtures/call-mcp.dot | 12 + .../fixtures/call-openapi-redirect.dot | 12 + tests/visualization/fixtures/call-openapi.dot | 12 + .../fixtures/conditional-task.dot | 14 + tests/visualization/fixtures/do-multiple.dot | 17 + tests/visualization/fixtures/do-single.dot | 14 + tests/visualization/fixtures/emit.dot | 12 + tests/visualization/fixtures/for.dot | 19 + tests/visualization/fixtures/fork.dot | 21 + .../fixtures/listen-to-all read-envelope.dot | 12 + .../visualization/fixtures/listen-to-all.dot | 12 + .../fixtures/listen-to-any-filter.dot | 12 + .../listen-to-any-forever-foreach.dot | 12 + .../listen-to-any-until-condition.dot | 12 + .../fixtures/listen-to-any-until-consumed.dot | 12 + .../visualization/fixtures/listen-to-any.dot | 12 + .../visualization/fixtures/listen-to-one.dot | 16 + .../fixtures/mock-service-extension.dot | 12 + tests/visualization/fixtures/raise-inline.dot | 12 + .../visualization/fixtures/raise-reusable.dot | 12 + .../fixtures/run-container-cleanup-always.dot | 12 + .../run-container-cleanup-eventually.dot | 12 + .../run-container-stdin-and-arguments.dot | 14 + .../fixtures/run-container-with-name.dot | 12 + .../visualization/fixtures/run-container.dot | 12 + .../visualization/fixtures/run-return-all.dot | 12 + .../fixtures/run-return-code.dot | 12 + .../fixtures/run-return-none.dot | 12 + .../fixtures/run-return-stderr.dot | 12 + .../run-script-with-stdin-and-arguments.dot | 12 + .../run-shell-stdin-and-arguments.dot | 14 + tests/visualization/fixtures/run-subflow.dot | 12 + .../visualization/fixtures/schedule-cron.dot | 12 + .../fixtures/schedule-event-driven.dot | 12 + .../visualization/fixtures/set-expression.dot | 12 + tests/visualization/fixtures/set.dot | 12 + .../fixtures/star-wars-homeworld.dot | 16 + .../fixtures/switch-then-string.dot | 28 + .../fixtures/try-catch-retry-inline.dot | 21 + .../fixtures/try-catch-retry-reusable.dot | 21 + .../visualization/fixtures/try-catch-then.dot | 34 + tests/visualization/fixtures/try-catch.dot | 21 + .../fixtures/wait-duration-inline.dot | 12 + .../fixtures/wait-duration-iso8601.dot | 12 + tests/visualization/test_graphviz.py | 58 + uv.lock | 1341 +++++++++++++ 176 files changed, 7508 insertions(+), 4801 deletions(-) create mode 100644 .gitmodules create mode 100644 .pre-commit-config.yaml delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 pyproject.toml delete mode 100644 serverlessworkflow/sdk/action.py delete mode 100644 serverlessworkflow/sdk/action_data_filter.py delete mode 100644 serverlessworkflow/sdk/auth_def.py create mode 100644 serverlessworkflow/sdk/authentication.py create mode 100644 serverlessworkflow/sdk/base.py delete mode 100644 serverlessworkflow/sdk/basic_props_def.py delete mode 100644 serverlessworkflow/sdk/bearer_props_def.py delete mode 100644 serverlessworkflow/sdk/branch.py delete mode 100644 serverlessworkflow/sdk/branch_timeout.py create mode 100644 serverlessworkflow/sdk/call_tasks.py delete mode 100644 serverlessworkflow/sdk/callback_state.py delete mode 100644 serverlessworkflow/sdk/callback_state_timeout.py delete mode 100644 serverlessworkflow/sdk/continue_as_def.py delete mode 100644 serverlessworkflow/sdk/correlation_def.py delete mode 100644 serverlessworkflow/sdk/cron_def.py delete mode 100644 serverlessworkflow/sdk/databased_switch_state.py delete mode 100644 serverlessworkflow/sdk/databased_switch_state_timeout.py delete mode 100644 serverlessworkflow/sdk/default_condition_def.py create mode 100644 serverlessworkflow/sdk/draw.py delete mode 100644 serverlessworkflow/sdk/end.py delete mode 100644 serverlessworkflow/sdk/end_data_condition.py delete mode 100644 serverlessworkflow/sdk/end_event_condition.py create mode 100644 serverlessworkflow/sdk/endpoint.py delete mode 100644 serverlessworkflow/sdk/error.py delete mode 100644 serverlessworkflow/sdk/error_def.py delete mode 100644 serverlessworkflow/sdk/event_based_switch_state.py delete mode 100644 serverlessworkflow/sdk/event_based_switch_state_timeout.py delete mode 100644 serverlessworkflow/sdk/event_data_filter.py delete mode 100644 serverlessworkflow/sdk/event_def.py delete mode 100644 serverlessworkflow/sdk/event_ref.py delete mode 100644 serverlessworkflow/sdk/event_state.py delete mode 100644 serverlessworkflow/sdk/event_state_timeout.py create mode 100644 serverlessworkflow/sdk/events.py delete mode 100644 serverlessworkflow/sdk/foreach_state.py delete mode 100644 serverlessworkflow/sdk/foreach_state_timeout.py delete mode 100644 serverlessworkflow/sdk/function.py delete mode 100644 serverlessworkflow/sdk/function_ref.py delete mode 100644 serverlessworkflow/sdk/inject_state.py delete mode 100644 serverlessworkflow/sdk/inject_state_timeout.py delete mode 100644 serverlessworkflow/sdk/metadata.py delete mode 100644 serverlessworkflow/sdk/oauth2props_def.py delete mode 100644 serverlessworkflow/sdk/on_events.py delete mode 100644 serverlessworkflow/sdk/operation_state.py delete mode 100644 serverlessworkflow/sdk/operation_state_timeout.py delete mode 100644 serverlessworkflow/sdk/parallel_state.py delete mode 100644 serverlessworkflow/sdk/parallel_state_timeout.py delete mode 100644 serverlessworkflow/sdk/produce_event_def.py create mode 100644 serverlessworkflow/sdk/retry.py delete mode 100644 serverlessworkflow/sdk/retry_def.py delete mode 100644 serverlessworkflow/sdk/schedule.py delete mode 100644 serverlessworkflow/sdk/serializable.py delete mode 100644 serverlessworkflow/sdk/sleep.py delete mode 100644 serverlessworkflow/sdk/sleep_state.py delete mode 100644 serverlessworkflow/sdk/sleep_state_timeout.py delete mode 100644 serverlessworkflow/sdk/start_def.py delete mode 100644 serverlessworkflow/sdk/state.py delete mode 100644 serverlessworkflow/sdk/state_data_filter.py delete mode 100644 serverlessworkflow/sdk/state_exec_timeout.py delete mode 100644 serverlessworkflow/sdk/state_machine_extensions.py delete mode 100644 serverlessworkflow/sdk/state_machine_generator.py delete mode 100644 serverlessworkflow/sdk/state_machine_helper.py delete mode 100644 serverlessworkflow/sdk/sub_flow_ref.py delete mode 100644 serverlessworkflow/sdk/swf_base.py create mode 100644 serverlessworkflow/sdk/tasks.py delete mode 100644 serverlessworkflow/sdk/transition.py delete mode 100644 serverlessworkflow/sdk/transition_data_condition.py delete mode 100644 serverlessworkflow/sdk/transition_event_condition.py delete mode 100644 serverlessworkflow/sdk/workflow_exec_timeout.py delete mode 100644 serverlessworkflow/sdk/workflow_time_out.py delete mode 100644 serverlessworkflow/sdk/workflow_validator.py delete mode 100644 setup.py create mode 160000 submodules/specification delete mode 100644 tests/examples/advertise-listing.json delete mode 100644 tests/examples/applicantrequest.json delete mode 100644 tests/examples/booklending.json delete mode 100644 tests/examples/carauctionbids.json delete mode 100644 tests/examples/checkcarvitals.json delete mode 100644 tests/examples/graph.json delete mode 100644 tests/examples/helloworld.json delete mode 100644 tests/examples/jobmonitoring.json delete mode 100644 tests/examples/parallel.json delete mode 100644 tests/examples/provisionorder.json delete mode 100644 tests/examples/second-subgraph.json delete mode 100644 tests/examples/sendcloudevent.json delete mode 100644 tests/examples/solvemathproblems.json delete mode 100644 tests/serverlessworkflow/__init__.py delete mode 100644 tests/serverlessworkflow/sdk/__init__.py delete mode 100644 tests/serverlessworkflow/sdk/test_action.py delete mode 100644 tests/serverlessworkflow/sdk/test_event_based_switch_state.py delete mode 100644 tests/serverlessworkflow/sdk/test_hydrate.py delete mode 100644 tests/serverlessworkflow/sdk/test_workflow.json delete mode 100644 tests/serverlessworkflow/sdk/test_workflow.py delete mode 100644 tests/serverlessworkflow/sdk/test_workflow.yaml delete mode 100644 tests/serverlessworkflow/sdk/test_workflow_validator.py create mode 100644 tests/specification/test_spec_examples.py create mode 100644 tests/specification/test_spec_examples_validations.py create mode 100644 tests/visualization/__init__.py create mode 100644 tests/visualization/fixtures/accumulate-room-readings.dot create mode 100644 tests/visualization/fixtures/authentication-bearer-uri-format.dot create mode 100644 tests/visualization/fixtures/authentication-bearer.dot create mode 100644 tests/visualization/fixtures/authentication-oauth2-secret.dot create mode 100644 tests/visualization/fixtures/authentication-oauth2.dot create mode 100644 tests/visualization/fixtures/authentication-oidc-secret.dot create mode 100644 tests/visualization/fixtures/authentication-oidc.dot create mode 100644 tests/visualization/fixtures/authentication-reusable.dot create mode 100644 tests/visualization/fixtures/call-asyncapi-publish.dot create mode 100644 tests/visualization/fixtures/call-asyncapi-subscribe-consume-amount.dot create mode 100644 tests/visualization/fixtures/call-asyncapi-subscribe-consume-forever-foreach.dot create mode 100644 tests/visualization/fixtures/call-asyncapi-subscribe-consume-until.dot create mode 100644 tests/visualization/fixtures/call-asyncapi-subscribe-consume-while.dot create mode 100644 tests/visualization/fixtures/call-custom-function-cataloged.dot create mode 100644 tests/visualization/fixtures/call-custom-function-inline.dot create mode 100644 tests/visualization/fixtures/call-grpc.dot create mode 100644 tests/visualization/fixtures/call-http-endpoint-interpolation-shorthand.dot create mode 100644 tests/visualization/fixtures/call-http-endpoint-interpolation.dot create mode 100644 tests/visualization/fixtures/call-http-query-headers-expressions.dot create mode 100644 tests/visualization/fixtures/call-http-query-parameters.dot create mode 100644 tests/visualization/fixtures/call-http-redirect.dot create mode 100644 tests/visualization/fixtures/call-mcp.dot create mode 100644 tests/visualization/fixtures/call-openapi-redirect.dot create mode 100644 tests/visualization/fixtures/call-openapi.dot create mode 100644 tests/visualization/fixtures/conditional-task.dot create mode 100644 tests/visualization/fixtures/do-multiple.dot create mode 100644 tests/visualization/fixtures/do-single.dot create mode 100644 tests/visualization/fixtures/emit.dot create mode 100644 tests/visualization/fixtures/for.dot create mode 100644 tests/visualization/fixtures/fork.dot create mode 100644 tests/visualization/fixtures/listen-to-all read-envelope.dot create mode 100644 tests/visualization/fixtures/listen-to-all.dot create mode 100644 tests/visualization/fixtures/listen-to-any-filter.dot create mode 100644 tests/visualization/fixtures/listen-to-any-forever-foreach.dot create mode 100644 tests/visualization/fixtures/listen-to-any-until-condition.dot create mode 100644 tests/visualization/fixtures/listen-to-any-until-consumed.dot create mode 100644 tests/visualization/fixtures/listen-to-any.dot create mode 100644 tests/visualization/fixtures/listen-to-one.dot create mode 100644 tests/visualization/fixtures/mock-service-extension.dot create mode 100644 tests/visualization/fixtures/raise-inline.dot create mode 100644 tests/visualization/fixtures/raise-reusable.dot create mode 100644 tests/visualization/fixtures/run-container-cleanup-always.dot create mode 100644 tests/visualization/fixtures/run-container-cleanup-eventually.dot create mode 100644 tests/visualization/fixtures/run-container-stdin-and-arguments.dot create mode 100644 tests/visualization/fixtures/run-container-with-name.dot create mode 100644 tests/visualization/fixtures/run-container.dot create mode 100644 tests/visualization/fixtures/run-return-all.dot create mode 100644 tests/visualization/fixtures/run-return-code.dot create mode 100644 tests/visualization/fixtures/run-return-none.dot create mode 100644 tests/visualization/fixtures/run-return-stderr.dot create mode 100644 tests/visualization/fixtures/run-script-with-stdin-and-arguments.dot create mode 100644 tests/visualization/fixtures/run-shell-stdin-and-arguments.dot create mode 100644 tests/visualization/fixtures/run-subflow.dot create mode 100644 tests/visualization/fixtures/schedule-cron.dot create mode 100644 tests/visualization/fixtures/schedule-event-driven.dot create mode 100644 tests/visualization/fixtures/set-expression.dot create mode 100644 tests/visualization/fixtures/set.dot create mode 100644 tests/visualization/fixtures/star-wars-homeworld.dot create mode 100644 tests/visualization/fixtures/switch-then-string.dot create mode 100644 tests/visualization/fixtures/try-catch-retry-inline.dot create mode 100644 tests/visualization/fixtures/try-catch-retry-reusable.dot create mode 100644 tests/visualization/fixtures/try-catch-then.dot create mode 100644 tests/visualization/fixtures/try-catch.dot create mode 100644 tests/visualization/fixtures/wait-duration-inline.dot create mode 100644 tests/visualization/fixtures/wait-duration-iso8601.dot create mode 100644 tests/visualization/test_graphviz.py create mode 100644 uv.lock diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 3f3ee52..311f459 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -7,28 +7,66 @@ on: branches: ["main"] jobs: - tests: - name: "Python 3.9" + lint: + name: "Lint (ruff, mypy)" runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4.2.2 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version-file: "pyproject.toml" + - name: Install dependencies + run: uv sync --all-extras + - name: Run ruff (linting) + run: uv run ruff check . + - name: Run ruff (formatting) + run: uv run ruff format --check . + - name: Run mypy + run: uv run mypy serverlessworkflow + test: + name: "Test (Python ${{ matrix.python-version }})" + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] steps: - uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4.2.2 - - uses: "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065" # v5.6.0 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: '3.9' + enable-cache: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} - name: Install graphviz run: | sudo apt-get update sudo apt-get install graphviz graphviz-dev - name: Install dependencies - run: | - pip install pipenv - pip install build - pipenv install --dev --system - pip install setuptools==70.3.0 - - name: Test - run: | - pipenv run pytest - - name: Build - run: | - python -m build + run: uv sync --all-extras + - name: Run pytest + run: uv run pytest + + build: + name: "Build package" + runs-on: "ubuntu-latest" + needs: [lint, test] + steps: + - uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4.2.2 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version-file: "pyproject.toml" + - name: Build package + run: uv build diff --git a/.gitignore b/.gitignore index 81b4242..39cd590 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ dmypy.json .idea .vscode *.iml + +tests/visualization/outputs/* +tests/visualization/fixtures/*.png diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..162f35a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/specification"] + path = submodules/specification + url = https://github.com/serverlessworkflow/specification.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..36dbb9e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,157 @@ +# Pre-commit hooks for Python projects +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +default_language_version: + python: python3.10 + +repos: + # General file checks +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + # Prevent giant files from being committed + - id: check-added-large-files + args: [--maxkb=1000] + # Check for files that would conflict in case-insensitive filesystems + - id: check-case-conflict + # Check for merge conflicts + - id: check-merge-conflict + # Check for debugger imports and py37+ breakpoint() + - id: debug-statements + # Check JSON files are valid + - id: check-json + # Check TOML files are valid + - id: check-toml + # Check YAML files are valid + - id: check-yaml + args: [--unsafe] # Allow custom YAML tags + # Ensure files end with a newline + - id: end-of-file-fixer + # Remove trailing whitespace + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + # Fix mixed line endings + - id: mixed-line-ending + args: [--fix=lf] + # Check docstrings are valid Python + - id: check-docstring-first + # Check for symlinks that point to nothing + - id: check-symlinks + # Detect private keys + - id: detect-private-key + + # Ruff - Fast Python linter and formatter (replaces Black, isort, flake8, pylint) +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.1 + hooks: + # Run the linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Run the formatter + - id: ruff-format + + # Type checking with mypy +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: + - types-PyYAML + - types-requests + args: [--ignore-missing-imports, --warn-unused-configs] + # Only run on serverlessworkflow package, not tests + files: ^serverlessworkflow/ + + # Security checks +- repo: https://github.com/PyCQA/bandit + rev: 1.7.6 + hooks: + - id: bandit + args: [-c, pyproject.toml] + additional_dependencies: ['bandit[toml]'] + # Skip tests directory + exclude: ^tests/ + + # # Check for common security issues + # - repo: https://github.com/Lucas-C/pre-commit-hooks-safety + # rev: v1.3.3 + # hooks: + # - id: python-safety-dependencies-check + # files: pyproject.toml + + # Markdown formatting +- repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-gfm # GitHub Flavored Markdown + - mdformat-black # Format code blocks with black style + args: [--wrap, '100'] + + # Check for spelling errors +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: [--ignore-words-list=crate] + exclude: ^(poetry.lock|package-lock.json|\.git/|\.pytest_cache/|\.mypy_cache/) + + # Validate GitHub Actions workflows +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.27.4 + hooks: + - id: check-github-workflows + - id: check-dependabot + + # YAML formatting +- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.12.0 + hooks: + - id: pretty-format-yaml + args: [--autofix, --indent, '2'] + exclude: ^(\.github/|examples/) # Exclude workflow files and examples + + # pyproject.toml formatting +- repo: https://github.com/tox-dev/pyproject-fmt + rev: 1.7.0 + hooks: + - id: pyproject-fmt + + # Check for outdated Python syntax +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [--py310-plus] + + # Check requirements files +- repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.4 + hooks: + - id: forbid-crlf + - id: remove-crlf + - id: forbid-tabs + - id: remove-tabs + + # Conventional commits +- repo: https://github.com/compilerla/conventional-pre-commit + rev: v3.0.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [--force-scope] + +# CI configuration +ci: + autofix_commit_msg: | + [pre-commit.ci] auto fixes from pre-commit.com hooks + + for more information, see https://pre-commit.ci + autofix_prs: true + autoupdate_branch: '' + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 122a90e..0000000 --- a/Pipfile +++ /dev/null @@ -1,18 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -jsonschema = "==4.4.0" -pyyaml = "==6.0" -requests = "*" -pygraphviz = "==1.11" -transitions = "==0.9.2" - -[dev-packages] -pytest = "==6.2.5" -pytest-runner = "==5.3.1" - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 441484f..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,345 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "f618c8a1f7bfa991732719216d5ac4c908d421c47ec2b6a0a13d7bca4d8f4da1" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", - "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" - ], - "markers": "python_version >= '3.8'", - "version": "==25.3.0" - }, - "certifi": { - "hashes": [ - "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", - "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b" - ], - "markers": "python_version >= '3.7'", - "version": "==2025.6.15" - }, - "charset-normalizer": { - "hashes": [ - "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", - "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", - "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", - "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", - "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", - "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", - "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", - "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", - "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", - "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", - "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", - "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", - "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", - "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", - "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", - "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", - "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", - "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", - "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", - "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", - "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", - "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", - "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", - "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", - "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", - "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", - "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", - "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", - "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", - "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", - "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", - "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", - "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", - "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", - "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", - "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", - "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", - "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", - "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", - "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", - "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", - "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", - "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", - "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", - "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", - "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", - "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", - "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", - "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", - "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", - "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", - "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", - "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", - "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", - "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", - "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", - "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", - "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", - "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", - "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", - "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", - "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", - "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", - "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", - "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", - "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", - "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", - "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", - "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", - "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", - "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", - "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", - "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", - "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", - "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", - "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", - "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", - "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", - "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", - "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", - "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", - "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", - "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", - "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", - "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", - "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", - "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", - "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", - "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", - "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", - "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", - "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.2" - }, - "idna": { - "hashes": [ - "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", - "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10" - }, - "jsonschema": { - "hashes": [ - "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83", - "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==4.4.0" - }, - "pygraphviz": { - "hashes": [ - "sha256:a97eb5ced266f45053ebb1f2c6c6d29091690503e3a5c14be7f908b37b06f2d4" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.11" - }, - "pyrsistent": { - "hashes": [ - "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", - "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", - "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", - "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", - "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", - "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", - "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", - "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", - "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", - "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", - "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", - "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", - "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", - "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", - "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", - "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", - "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", - "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", - "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", - "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", - "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", - "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", - "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", - "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", - "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", - "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", - "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", - "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", - "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", - "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", - "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", - "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" - ], - "markers": "python_version >= '3.8'", - "version": "==0.20.0" - }, - "pyyaml": { - "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "requests": { - "hashes": [ - "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", - "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.4" - }, - "six": { - "hashes": [ - "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", - "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.17.0" - }, - "transitions": { - "hashes": [ - "sha256:2f8490dbdbd419366cef1516032ab06d07ccb5839ef54905e842a472692d4204", - "sha256:f7b40c9b4a93869f36c4d1c33809aeb18cdeeb065fd1adba018ee39c3db216f3" - ], - "index": "pypi", - "version": "==0.9.2" - }, - "urllib3": { - "hashes": [ - "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", - "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" - ], - "markers": "python_version >= '3.9'", - "version": "==2.5.0" - } - }, - "develop": { - "attrs": { - "hashes": [ - "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", - "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" - ], - "markers": "python_version >= '3.8'", - "version": "==25.3.0" - }, - "iniconfig": { - "hashes": [ - "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", - "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" - ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" - }, - "packaging": { - "hashes": [ - "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", - "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" - ], - "markers": "python_version >= '3.8'", - "version": "==25.0" - }, - "pluggy": { - "hashes": [ - "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", - "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" - ], - "markers": "python_version >= '3.9'", - "version": "==1.6.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pytest": { - "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.2.5" - }, - "pytest-runner": { - "hashes": [ - "sha256:0fce5b8dc68760f353979d99fdd6b3ad46330b6b1837e2077a89ebcf204aac91", - "sha256:85f93af814438ee322b4ea08fe3f5c2ad53b253577f3bd84b2ad451fee450ac5" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==5.3.1" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" - } - } -} diff --git a/README.md b/README.md index 5916476..fe19d2e 100644 --- a/README.md +++ b/README.md @@ -9,158 +9,172 @@ With the SDK you can: ### Status -Current sdk version conforms to the [Serverless Workflow specification v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x). +Current sdk version conforms to the [Serverless Workflow specification v1.0](https://github.com/serverlessworkflow/specification/tree/v1.0.0). +### Install and use -## Install dependencies and run test - -- Python 3 required - -- pipenv required `pip install pipenv` +- Python 3.10+ required +```bash +pip install serverlessworkflow ``` -pipenv install --dev - -pipenv run pip install 'setuptools==70.3.0' -pipenv shell - -python setup.py pytest -``` - -## Programmatically build workflow definitions +```python +from serverlessworkflow.sdk import ( + Workflow, + Document, + CallHttpTask, + CallHttpArguments, +) -``` - workflow = Workflow( - id="greeting", - name="Greeting Workflow", - description="Greet Someone", - version='1.0', - specVersion='0.8', - start="Greet", - states=[ - OperationState( - name="Greet", - type="operation", - actions=[ - Action( - functionRef=FunctionRef( - refName="greetingFunction", - arguments={ - "name": "${ .person.name }" - } - ), - actionDataFilter=ActionDataFilter( - results="${ .greeting }" +new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="http-query-params", + version="1.0.0" + ), + do=[ + { + "searchStarWarsCharacters": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://swapi.dev/api/people/", + query={"search": "${.searchQuery}"}, + ) ) - ) + } ], - end=True + input={ + "schema": { + "format": "json", + "document": { + "type": "object", + "required": ["searchQuery"], + "properties": {"searchQuery": {"type": "string"}}, + }, + } + }, ) - ], - functions=[ - Function(name="greetingFunction", - operation="file://myapis/greetingapis.json#greeting") - ] - ) ``` -You can see a full example in the [test_workflow.py](tests/serverlessworkflow/sdk/test_workflow.py) file -## Parse workflow JSON and YAML definitions +## Programmatically build workflow definitions -### Convert from JSON or YAML source +```python +from serverlessworkflow.sdk import ( + Workflow, + Document, + CallHttpTask, + CallHttpArguments, +) +workflow = Workflow( + document=Document( + dsl="1.0.0", + namespace="default", + name="greeting", + version="1.0.0", + title="Greeting Workflow", + summary="Greet Someone", + ), + do=[ + CallHttpTask( + call="http", + with_=CallHttpArguments( + method="get", + endpoint="https://api.example.com/greet?name={$input.name}" + ) + ) + ] +) ``` -swf_content = """id: greeting -name: Greeting Workflow -version: '1.0' -description: Greet Someone -specVersion: '0.8' -start: Greet -states: -- name: Greet - type: operation - actions: - - functionRef: - refName: greetingFunction - arguments: - name: ${ .person.name } - actionDataFilter: - results: ${ .greeting } - end: true -functions: -- name: greetingFunction - operation: file://myapis/greetingapis.json#greeting -""" - workflow = Workflow.from_source(swf_content) -``` +You can see full examples in the [tests/specification](tests/specification) directory -You can see a full example in the [test_workflow.py](tests/serverlessworkflow/sdk/test_workflow.py) file +## Parse workflow JSON and YAML definitions +### Load from YAML source -### Parse workflow to JSON / YAML +```python +from serverlessworkflow.sdk import Workflow + +yaml_content = """ +document: + dsl: 1.0.0-alpha1 + namespace: default + name: greeting + version: 1.0.0 +do: + - call: http + with: + method: get + endpoint: https://api.example.com/greet +""" -``` -workflow = Workflow(id_="greeting", - name="Greeting Workflow", - description="Greet Someone", - version='1.0', - specVersion='0.8', - start="Greet", - states=[], - functions=[] -) -print(workflow.to_json()) -print(workflow.to_yaml()) +workflow = Workflow.from_yaml(yaml_content) ``` -You can see a full example in the [test_workflow.py](tests/serverlessworkflow/sdk/test_workflow.py) file +You can see full examples in the [tests/specification](tests/specification) directory -## Validate workflow definitions +### Export workflow to YAML -``` -workflow = Workflow(id_="greeting", - name="Greeting Workflow", - description="Greet Someone", - version='1.0', - specVersion='0.8', - start="Greet", - states=[], - functions=[] +```python +from serverlessworkflow.sdk import Workflow, Document, SetTask + +workflow = Workflow( + document=Document( + dsl="1.0.0-alpha1", + namespace="default", + name="greeting", + version="1.0.0", + ), + do=[ + SetTask(set={"greeting": "Hello World"}) + ] ) -WorkflowValidator(Workflow(workflow)).validate() +yaml_output = workflow.to_yaml() +print(yaml_output) ``` -The `validate` method will raise an exception if the provided workflow does not complaint specification. -You can see a full example in the [test_workflow_validator](tests/serverlessworkflow/sdk/test_workflow_validator.py) file +You can see full examples in the [tests/specification](tests/specification) directory ## Generate workflow state machine and graph +**Note** Please note that `pip install serverlessworkflow[viz]` needs to be installed in order for this to work. The `viz` feature installs pydot, which supports `graphviz`. + To generate the workflow graph diagram: +To dot files: +```python +workflow.render_graph(filename="/tmp/out.dot") +``` + +The following requires `graphviz` to be installed (verify by checking to see if the `dot` binary is on the PATH): + +To png files: ```python -from serverlessworkflow.sdk.workflow import Workflow -from serverlessworkflow.sdk.state_machine_helper import StateMachineHelper - -def main(): - subflows = [] - with open("tests/examples/graph.json") as f: - workflow = Workflow.from_source(f.read()) - with open("tests/examples/advertise-listing.json") as f: - subflows.append(Workflow.from_source(f.read())) - with open("tests/examples/second-subgraph.json") as f: - subflows.append(Workflow.from_source(f.read())) - machine_helper = StateMachineHelper(workflow=workflow, get_actions=True, subflows=subflows) - machine_helper.draw('diagram.svg') - - -if __name__ == "__main__": - main() +workflow.render_graph(filename="/tmp/out.png") ``` -The `StateMachineHelper` can be set with `get_actions` as `False` and the produced diagram will not represent the actions inside each state (it will only create a diagram with the states and their transitions). Moreover, the developer may not give any `subflows`, and they simply will not be generated. -As for the `draw` method, the developer can also specify `graph_engine='mermaid'`. In that case, the method will not generate a figure, but rather the Mermaid code that can be executed, for instance, in the [Mermaid Live Editor](https://mermaid.live). +# Local development + +## Install dependencies and run tests + +- [uv](https://docs.astral.sh/uv/) recommended for dependency management + +```bash + +# Install dependencies +uv sync --all-extras +uv pip install -e .[dev,viz] + +# Run tests +uv run pytest + +# Run linting +uv run ruff check . -It is also possible to only generate the workflow state machine. An example on how to do so can be analyzed in the [state_machine_helper](serverlessworkflow/sdk/state_machine_helper.py) source code. +# Run type checking +uv run mypy . +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3775d99 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,172 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61", + "wheel", +] + +[project] +name = "serverlessworkflow-sdk" +version = "1.0.0" # TODO: Update to 2.0.0 for spec 1.0 migration +description = "Serverless Workflow Specification - Python SDK" +readme = "README.md" +keywords = [ + "cloud", + "cncf", + "dsl", + "orchestration", + "serverless", + "workflow", +] +license = "Apache-2.0" +maintainers = [ + {name = "Serverless Workflow Contributors"}, +] +authors = [ + {name = "Serverless Workflow Contributors"}, +] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Distributed Computing", +] +dependencies = [ + "jsonschema<5.0,>=4.4", + "pyyaml<7.0,>=6", + "requests>=2.25", + "transitions<1.0,>=0.9.2", +] +[project.optional-dependencies] +all = [ + "serverlessworkflow.sdk[dev,docs]", +] +dev = [ + "bandit[toml]>=1.7", + "mypy>=1", + "pytest>=7", + "pytest-cov>=4", + "pytest-xdist>=3", + "ruff>=0.1", + "types-PyYAML", + "types-requests", +] +docs = [ + "myst-parser>=2", # Markdown support in Sphinx + "sphinx>=7", + "sphinx-rtd-theme>=2", +] +viz = [ + "pydot>=4.0.1", +] +[project.urls] +Changelog = "https://github.com/serverlessworkflow/sdk-python/releases" +Documentation = "https://github.com/serverlessworkflow/sdk-python/blob/main/README.md" +Homepage = "https://serverlessworkflow.io/" +Issues = "https://github.com/serverlessworkflow/sdk-python/issues" +Repository = "https://github.com/serverlessworkflow/sdk-python" + +[tool.setuptools] +packages = ["serverlessworkflow", "serverlessworkflow.sdk"] + +[tool.setuptools.package-data] +serverlessworkflow = ["py.typed"] # Mark as PEP 561 compliant (typed package) + +# Pytest configuration + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort (import sorting) + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "B905", # zip strict (Python 3.10+) +] +extend-select = ["D"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # Allow unused imports in __init__.py +"tests/*" = ["ARG", "SIM"] # Allow unused args and complex logic in tests + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint.pydocstyle] +convention = "google" + +# Bandit configuration (security linting) + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--verbose", + "--strict-markers", + "--strict-config", + "--cov=serverlessworkflow", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", + "unit: marks tests as unit tests", + "spec_example: marks tests that validate against spec examples", +] + +# Coverage configuration + +[tool.coverage.run] +source = ["serverlessworkflow"] +omit = [ + "*/tests/*", + "*/test_*.py", + "*/__pycache__/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "@abstractmethod", +] + +# Ruff configuration (modern linter/formatter, replaces black + flake8 + isort) + +[tool.bandit] +exclude_dirs = ["tests", "test_*.py", "*_test.py"] +tests = ["B201", "B301"] +skips = ["B101", "B601"] + +[tool.bandit.assert_used] +skips = ["*_test.py", "test_*.py"] diff --git a/serverlessworkflow/__init__.py b/serverlessworkflow/__init__.py index e69de29..6def674 100644 --- a/serverlessworkflow/__init__.py +++ b/serverlessworkflow/__init__.py @@ -0,0 +1 @@ +"""Serverless Workflow SDK for Python.""" diff --git a/serverlessworkflow/sdk/__init__.py b/serverlessworkflow/sdk/__init__.py index e69de29..145e304 100644 --- a/serverlessworkflow/sdk/__init__.py +++ b/serverlessworkflow/sdk/__init__.py @@ -0,0 +1,245 @@ +"""Serverless Workflow SDK v1 - Python classes for Serverless Workflow DSL 1.0.""" + +# Base types and classes +# Authentication +from serverlessworkflow.sdk.authentication import ( + BasicAuthenticationConfiguration, + BasicAuthenticationPolicy, + BearerAuthenticationConfiguration, + BearerAuthenticationPolicy, + DigestAuthenticationConfiguration, + DigestAuthenticationPolicy, + OAuth2AuthenticationConfiguration, + OAuth2AuthenticationPolicy, + OAuth2Client, + OAuth2Endpoints, + OAuth2Token, + OAuth2TokenRequest, + OpenIdConnectAuthenticationPolicy, + ReferenceableAuthenticationPolicy, +) +from serverlessworkflow.sdk.base import ( + Duration, + Error, + ErrorFilter, + Export, + ExternalResource, + FlowDirective, + Input, + Output, + RuntimeExpression, + Schema, + TaskBase, + TaskItem, + Timeout, + UriTemplate, +) + +# Call tasks +from serverlessworkflow.sdk.call_tasks import ( + AsyncApiInboundMessage, + AsyncApiMessageConsumptionPolicy, + AsyncApiOutboundMessage, + AsyncApiServer, + AsyncApiSubscription, + CallA2AArguments, + CallA2ATask, + CallAsyncApiArguments, + CallAsyncApiTask, + CallFunctionTask, + CallGrpcArguments, + CallGrpcTask, + CallHttpArguments, + CallHttpTask, + CallMcpArguments, + CallMcpTask, + CallOpenApiArguments, + CallOpenApiTask, + CallTask, + GrpcService, + McpClient, + McpHttpTransport, + McpStdioTransport, + McpTransport, + SubscriptionIterator, +) + +# Endpoints +from serverlessworkflow.sdk.endpoint import ( + Catalog, + Endpoint, +) + +# Events +from serverlessworkflow.sdk.events import ( + Correlation, + EventConsumptionStrategy, + EventConsumptionStrategyAll, + EventConsumptionStrategyAny, + EventConsumptionStrategyOne, + EventFilter, + EventProperties, +) + +# Retry policies +from serverlessworkflow.sdk.retry import ( + ConstantBackoff, + ExponentialBackoff, + LinearBackoff, + RetryJitter, + RetryLimit, + RetryLimitAttempt, + RetryPolicy, +) + +# Tasks +from serverlessworkflow.sdk.tasks import ( + CatchConfiguration, + ContainerConfiguration, + ContainerLifetime, + DoTask, + EmitConfiguration, + EmitEventConfiguration, + EmitTask, + ForConfiguration, + ForkConfiguration, + ForkTask, + ForTask, + ListenConfiguration, + ListenTask, + RaiseConfiguration, + RaiseTask, + RunConfiguration, + RunTask, + ScriptConfiguration, + SetTask, + ShellConfiguration, + SwitchCase, + SwitchTask, + Task, + TryTask, + WaitTask, + WorkflowConfiguration, +) + +# Workflow +from serverlessworkflow.sdk.workflow import ( + Document, + Extension, + Schedule, + Use, + Workflow, +) + +__all__ = [ + # Base + "RuntimeExpression", + "UriTemplate", + "Duration", + "ExternalResource", + "Schema", + "Input", + "Output", + "Export", + "Timeout", + "Error", + "ErrorFilter", + "FlowDirective", + "TaskBase", + "TaskItem", + # Authentication + "OAuth2Token", + "OAuth2Client", + "OAuth2TokenRequest", + "OAuth2Endpoints", + "BasicAuthenticationConfiguration", + "BasicAuthenticationPolicy", + "BearerAuthenticationConfiguration", + "BearerAuthenticationPolicy", + "DigestAuthenticationConfiguration", + "DigestAuthenticationPolicy", + "OAuth2AuthenticationConfiguration", + "OAuth2AuthenticationPolicy", + "OpenIdConnectAuthenticationPolicy", + "ReferenceableAuthenticationPolicy", + # Endpoints + "Endpoint", + "Catalog", + # Events + "EventProperties", + "Correlation", + "EventFilter", + "EventConsumptionStrategyOne", + "EventConsumptionStrategyAny", + "EventConsumptionStrategyAll", + "EventConsumptionStrategy", + # Retry + "ConstantBackoff", + "ExponentialBackoff", + "LinearBackoff", + "RetryLimitAttempt", + "RetryLimit", + "RetryJitter", + "RetryPolicy", + # Call tasks + "AsyncApiServer", + "AsyncApiOutboundMessage", + "AsyncApiInboundMessage", + "SubscriptionIterator", + "AsyncApiMessageConsumptionPolicy", + "AsyncApiSubscription", + "CallAsyncApiArguments", + "CallAsyncApiTask", + "GrpcService", + "CallGrpcArguments", + "CallGrpcTask", + "CallHttpArguments", + "CallHttpTask", + "CallOpenApiArguments", + "CallOpenApiTask", + "CallA2AArguments", + "CallA2ATask", + "McpHttpTransport", + "McpStdioTransport", + "McpTransport", + "McpClient", + "CallMcpArguments", + "CallMcpTask", + "CallFunctionTask", + "CallTask", + # Tasks + "DoTask", + "ForkConfiguration", + "ForkTask", + "EmitEventConfiguration", + "EmitConfiguration", + "EmitTask", + "ForConfiguration", + "ForTask", + "ListenConfiguration", + "ListenTask", + "RaiseConfiguration", + "RaiseTask", + "ContainerLifetime", + "ContainerConfiguration", + "ScriptConfiguration", + "ShellConfiguration", + "WorkflowConfiguration", + "RunConfiguration", + "RunTask", + "SetTask", + "SwitchCase", + "SwitchTask", + "CatchConfiguration", + "TryTask", + "WaitTask", + "Task", + # Workflow + "Document", + "Extension", + "Use", + "Schedule", + "Workflow", +] + +__version__ = "1.0.0" diff --git a/serverlessworkflow/sdk/action.py b/serverlessworkflow/sdk/action.py deleted file mode 100644 index caeb6f6..0000000 --- a/serverlessworkflow/sdk/action.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.action_data_filter import ActionDataFilter -from serverlessworkflow.sdk.event_ref import EventRef -from serverlessworkflow.sdk.function_ref import FunctionRef -from serverlessworkflow.sdk.sleep import Sleep -from serverlessworkflow.sdk.sub_flow_ref import SubFlowRef -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, UnionTypeOf, SimpleTypeOf, HydratableParameter, SwfBase - - -class Action(SwfBase): - id: str = None - name: str = None - functionRef: (str | FunctionRef) = None - eventRef: EventRef = None - subFlowRef: (str | SubFlowRef) = None - sleep: Sleep = None - retryRef: str = None - nonRetryableErrors: [str] = None - retryableErrors: [str] = None - actionDataFilter: ActionDataFilter = None - condition: str = None - - def __init__(self, - id: str = None, - name: str = None, - functionRef: (str | FunctionRef) = None, - eventRef: EventRef = None, - subFlowRef: (str | SubFlowRef) = None, - sleep: Sleep = None, - retryRef: str = None, - nonRetryableErrors: [str] = None, - retryableErrors: [str] = None, - actionDataFilter: ActionDataFilter = None, - condition: str = None, - eslavida: str = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, Action.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - parameter = HydratableParameter(value=p_value) - if p_key == 'functionRef': - return parameter.hydrateAs(UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(FunctionRef)])) - if p_key == 'eventRef': - return parameter.hydrateAs(ComplexTypeOf(EventRef)) - if p_key == 'subFlowRef': - return parameter.hydrateAs(UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(SubFlowRef)])) - if p_key == 'sleep': - return parameter.hydrateAs(ComplexTypeOf(Sleep)) - if p_key == 'actionDataFilter': - return parameter.hydrateAs(ComplexTypeOf(ActionDataFilter)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/action_data_filter.py b/serverlessworkflow/sdk/action_data_filter.py deleted file mode 100644 index 5b0ce67..0000000 --- a/serverlessworkflow/sdk/action_data_filter.py +++ /dev/null @@ -1,16 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class ActionDataFilter(SwfBase): - fromStateData: str = None - useResults: bool = None - results: str = None - toStateData: str = None - - def __init__(self, - fromStateData: str = None, - useResults: bool = None, - results: str = None, - toStateData: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/auth_def.py b/serverlessworkflow/sdk/auth_def.py deleted file mode 100644 index 06ac997..0000000 --- a/serverlessworkflow/sdk/auth_def.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.basic_props_def import BasicPropsDef -from serverlessworkflow.sdk.bearer_props_def import BearerPropsDef -from serverlessworkflow.sdk.oauth2props_def import Oauth2PropsDef -from serverlessworkflow.sdk.swf_base import SwfBase - - -class AuthDef(SwfBase): - name: str = None - scheme: str = None - properties: (str | (BasicPropsDef | BearerPropsDef | Oauth2PropsDef)) = None - - def __init__(self, - name: str = None, - scheme: str = None, - properties: (str | (BasicPropsDef | BearerPropsDef | Oauth2PropsDef)) = None, - **kwargs): - - _default_values = {'scheme': 'basic'} - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration, _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - result = copy.deepcopy(p_value) - - if p_key == 'properties': - if p_value["username"] and p_value["password"]: - return BasicPropsDef(p_value) - if p_value["token"]: - return BearerPropsDef(p_value) - if p_value["grantType"]: - return Oauth2PropsDef(p_value) - - return result diff --git a/serverlessworkflow/sdk/authentication.py b/serverlessworkflow/sdk/authentication.py new file mode 100644 index 0000000..186f8a6 --- /dev/null +++ b/serverlessworkflow/sdk/authentication.py @@ -0,0 +1,167 @@ +"""Authentication policy classes for Serverless Workflow SDK v1.""" + +from dataclasses import dataclass + +from serverlessworkflow.sdk.base import UriTemplate + + +@dataclass +class OAuth2Token: + """Represents an OAuth2 token.""" + + token: str + type: str + + +@dataclass +class OAuth2Client: + """Definition of an OAuth2 client.""" + + id: str + secret: str | None = None + assertion: str | None = None + authentication: str = "client_secret_post" + + +@dataclass +class OAuth2TokenRequest: + """Configuration of an OAuth2 token request.""" + + encoding: str = "application/x-www-form-urlencoded" + + +@dataclass +class OAuth2Endpoints: + """Endpoint configurations for OAuth2.""" + + token: str = "/oauth2/token" + revocation: str = "/oauth2/revoke" + introspection: str = "/oauth2/introspect" + + +@dataclass +class BasicAuthenticationConfiguration: + """Configuration for basic authentication.""" + + username: str | None = None + password: str | None = None + use: str | None = None + + def __post_init__(self): + """Validate basic authentication configuration.""" + has_inline = self.username is not None and self.password is not None + has_secret = self.use is not None + if not (has_inline or has_secret): + raise ValueError("Either username/password or use must be specified") + if has_inline and has_secret: + raise ValueError("Cannot specify both inline credentials and secret reference") + + +@dataclass +class BasicAuthenticationPolicy: + """Basic authentication policy.""" + + basic: BasicAuthenticationConfiguration + + +@dataclass +class BearerAuthenticationConfiguration: + """Configuration for bearer authentication.""" + + token: str | None = None + use: str | None = None + + def __post_init__(self): + """Validate bearer authentication configuration.""" + has_inline = self.token is not None + has_secret = self.use is not None + if not (has_inline or has_secret): + raise ValueError("Either token or use must be specified") + if has_inline and has_secret: + raise ValueError("Cannot specify both inline token and secret reference") + + +@dataclass +class BearerAuthenticationPolicy: + """Bearer authentication policy.""" + + bearer: BearerAuthenticationConfiguration + + +@dataclass +class DigestAuthenticationConfiguration: + """Configuration for digest authentication.""" + + username: str | None = None + password: str | None = None + use: str | None = None + + def __post_init__(self): + """Validate digest authentication configuration.""" + has_inline = self.username is not None and self.password is not None + has_secret = self.use is not None + if not (has_inline or has_secret): + raise ValueError("Either username/password or use must be specified") + if has_inline and has_secret: + raise ValueError("Cannot specify both inline credentials and secret reference") + + +@dataclass +class DigestAuthenticationPolicy: + """Digest authentication policy.""" + + digest: DigestAuthenticationConfiguration + + +@dataclass +class OAuth2AuthenticationConfiguration: + """Configuration for OAuth2 authentication.""" + + authority: UriTemplate | None = None + grant: str | None = None + client: OAuth2Client | None = None + request: OAuth2TokenRequest | None = None + issuers: list[str] | None = None + scopes: list[str] | None = None + audiences: list[str] | None = None + username: str | None = None + password: str | None = None + subject: OAuth2Token | None = None + actor: OAuth2Token | None = None + endpoints: OAuth2Endpoints | None = None + use: str | None = None + + +@dataclass +class OAuth2AuthenticationPolicy: + """OAuth2 authentication policy.""" + + oauth2: OAuth2AuthenticationConfiguration + + +@dataclass +class OpenIdConnectAuthenticationPolicy: + """OpenID Connect authentication policy.""" + + oidc: OAuth2AuthenticationConfiguration + + +@dataclass +class ReferenceableAuthenticationPolicy: + """Referenceable authentication policy - either a reference or an inline policy.""" + + use: str | None = None + basic: BasicAuthenticationConfiguration | None = None + bearer: BearerAuthenticationConfiguration | None = None + digest: DigestAuthenticationConfiguration | None = None + oauth2: OAuth2AuthenticationConfiguration | None = None + oidc: OAuth2AuthenticationConfiguration | None = None + + def __post_init__(self): + """Validate authentication policy configuration.""" + policies = [self.basic, self.bearer, self.digest, self.oauth2, self.oidc] + set_policies = [p for p in policies if p is not None] + if self.use is not None and len(set_policies) > 0: + raise ValueError("Cannot specify both 'use' reference and inline policy") + if self.use is None and len(set_policies) != 1: + raise ValueError("Must specify either 'use' reference or exactly one inline policy") diff --git a/serverlessworkflow/sdk/base.py b/serverlessworkflow/sdk/base.py new file mode 100644 index 0000000..ae39301 --- /dev/null +++ b/serverlessworkflow/sdk/base.py @@ -0,0 +1,170 @@ +"""Base classes and types for Serverless Workflow SDK v1.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from serverlessworkflow.sdk.endpoint import Endpoint + from serverlessworkflow.sdk.tasks import Task + + +@dataclass +class RuntimeExpression: + """A runtime expression pattern: ${...}.""" + + expression: str + + def __post_init__(self): + """Validate runtime expression format.""" + if not self.expression.startswith("${") or not self.expression.endswith("}"): + raise ValueError(f"Runtime expression must match pattern ${{...}}: {self.expression}") + + +@dataclass +class UriTemplate: + """A URI or URI template.""" + + uri: str + + def __post_init__(self): + """Validate URI format.""" + # Basic validation that it looks like a URI with scheme + if "://" not in self.uri: + raise ValueError(f"URI must contain a scheme (protocol://): {self.uri}") + + +@dataclass +class Duration: + """Represents a duration in various formats.""" + + days: int | None = None + hours: int | None = None + minutes: int | None = None + seconds: int | None = None + milliseconds: int | None = None + iso8601: str | None = None + expression: RuntimeExpression | None = None + + def __post_init__(self): + """Validate that at least one duration field is set.""" + # At least one field must be set + if not any( + [ + self.days, + self.hours, + self.minutes, + self.seconds, + self.milliseconds, + self.iso8601, + self.expression, + ] + ): + raise ValueError("At least one duration field must be set") + + +@dataclass +class ExternalResource: + """Represents an external resource.""" + + endpoint: Endpoint + name: str | None = None + + +@dataclass +class Schema: + """Represents a schema definition.""" + + format: str = "json" + document: Any | None = None + resource: ExternalResource | None = None + + def __post_init__(self): + """Validate that exactly one of document or resource is specified.""" + if self.document is None and self.resource is None: + raise ValueError("Either document or resource must be specified") + if self.document is not None and self.resource is not None: + raise ValueError("Only one of document or resource can be specified") + + +@dataclass +class Input: + """Configures the input of a workflow or task.""" + + schema: Schema | None = None + from_: str | dict[str, Any] | None = field(default=None, metadata={"alias": "from"}) + + +@dataclass +class Output: + """Configures the output of a workflow or task.""" + + schema: Schema | None = None + as_: str | dict[str, Any] | None = field(default=None, metadata={"alias": "as"}) + + +@dataclass +class Export: + """Configures export to context.""" + + schema: Schema | None = None + as_: str | dict[str, Any] | None = field(default=None, metadata={"alias": "as"}) + + +@dataclass +class Timeout: + """The definition of a timeout.""" + + after: Duration + + +@dataclass +class Error: + """Represents an error.""" + + type: UriTemplate | RuntimeExpression + status: int + instance: str | RuntimeExpression | None = None + title: str | RuntimeExpression | None = None + detail: str | RuntimeExpression | None = None + + +@dataclass +class ErrorFilter: + """Error filtering based on static values.""" + + type: str | None = None + status: int | None = None + instance: str | None = None + title: str | None = None + details: str | None = None + + def __post_init__(self): + """Validate that at least one error filter field is set.""" + if not any([self.type, self.status, self.instance, self.title, self.details]): + raise ValueError("At least one error filter field must be set") + + +FlowDirective = str # 'continue', 'exit', 'end', or task name + + +@dataclass +class TaskBase: + """Base class for all tasks.""" + + if_: str | None = field(default=None, metadata={"alias": "if"}) + input: Input | None = None + output: Output | None = None + export: Export | None = None + timeout: Timeout | str | None = None + then: FlowDirective | None = None + metadata: dict[str, Any] | None = None + + +@dataclass +class TaskItem: + """A named task item.""" + + name: str + task: Task diff --git a/serverlessworkflow/sdk/basic_props_def.py b/serverlessworkflow/sdk/basic_props_def.py deleted file mode 100644 index ff30140..0000000 --- a/serverlessworkflow/sdk/basic_props_def.py +++ /dev/null @@ -1,15 +0,0 @@ -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import SwfBase - - -class BasicPropsDef(SwfBase): - username: str = None - password: str = None - metadata: Metadata = None - - def __init__(self, - username: str = None, - password: str = None, - metadata: Metadata = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/bearer_props_def.py b/serverlessworkflow/sdk/bearer_props_def.py deleted file mode 100644 index c479217..0000000 --- a/serverlessworkflow/sdk/bearer_props_def.py +++ /dev/null @@ -1,13 +0,0 @@ -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import SwfBase - - -class BearerPropsDef(SwfBase): - token: str = None - metadata: Metadata = None - - def __init__(self, - token: str = None, - metadata: Metadata = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/branch.py b/serverlessworkflow/sdk/branch.py deleted file mode 100644 index 8b10435..0000000 --- a/serverlessworkflow/sdk/branch.py +++ /dev/null @@ -1,30 +0,0 @@ -import copy - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.branch_timeout import BranchTimeOut -from serverlessworkflow.sdk.swf_base import ArrayTypeOf, ComplexTypeOf, HydratableParameter, SwfBase - - -class Branch(SwfBase): - name: str = None - timeouts: BranchTimeOut = None - actions: [Action] = None - - def __init__(self, - name: str = None, - timeouts: BranchTimeOut = None, - actions: [Action] = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, Branch.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(BranchTimeOut)) - - if p_key == 'actions': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Action)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/branch_timeout.py b/serverlessworkflow/sdk/branch_timeout.py deleted file mode 100644 index 141d41b..0000000 --- a/serverlessworkflow/sdk/branch_timeout.py +++ /dev/null @@ -1,12 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class BranchTimeOut(SwfBase): - actionExecTimeOut: str = None # ActionExecTimeOut - branchExecTimeOut: str = None # BranchExecTimeOut - - def __init__(self, - actionExecTimeOut: str = None, - branchExecTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/call_tasks.py b/serverlessworkflow/sdk/call_tasks.py new file mode 100644 index 0000000..58337e8 --- /dev/null +++ b/serverlessworkflow/sdk/call_tasks.py @@ -0,0 +1,340 @@ +"""Call task classes for Serverless Workflow SDK v1.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from serverlessworkflow.sdk.base import Export, Output, TaskItem + +from serverlessworkflow.sdk.authentication import ReferenceableAuthenticationPolicy +from serverlessworkflow.sdk.base import Duration, ExternalResource, TaskBase +from serverlessworkflow.sdk.endpoint import Endpoint + +# AsyncAPI related classes + + +@dataclass +class AsyncApiServer: + """Configures the target server of an AsyncAPI operation.""" + + name: str + variables: dict[str, Any] | None = None + + +@dataclass +class AsyncApiOutboundMessage: + """Message configuration for AsyncAPI publish operations.""" + + payload: dict[str, Any] | None = None + headers: dict[str, Any] | None = None + + +@dataclass +class AsyncApiInboundMessage: + """Message consumed by an AsyncAPI subscription.""" + + payload: dict[str, Any] | None = None + headers: dict[str, Any] | None = None + correlationId: str | None = None + + +@dataclass +class SubscriptionIterator: + """Configures iteration over consumed items.""" + + item: str | None = None + at: str | None = None + do: list[TaskItem] | None = None + output: Output | None = None + export: Export | None = None + + +@dataclass +class AsyncApiMessageConsumptionPolicy: + """Message consumption policy for AsyncAPI subscriptions.""" + + amount: int | None = None + while_: str | None = field(default=None, metadata={"alias": "while"}) + until: str | None = None # Runtime expression + for_: Duration | None = field(default=None, metadata={"alias": "for"}) + + def __post_init__(self): + """Validate consumption policy configuration.""" + policies = [self.amount, self.while_, self.until] + set_policies = [p for p in policies if p is not None] + if len(set_policies) != 1: + raise ValueError("Must specify exactly one of: amount, while, or until") + + +@dataclass +class AsyncApiSubscription: + """Subscription configuration for AsyncAPI operations.""" + + consume: AsyncApiMessageConsumptionPolicy + filter: str | None = None # Runtime expression + foreach: SubscriptionIterator | None = None + + +@dataclass +class CallAsyncApiArguments: + """Arguments for AsyncAPI call.""" + + document: ExternalResource + operation: str | None = None + channel: str | None = None + server: AsyncApiServer | None = None + protocol: str | None = None + message: AsyncApiOutboundMessage | None = None + subscription: AsyncApiSubscription | None = None + authentication: ReferenceableAuthenticationPolicy | None = None + + def __post_init__(self): + """Validate AsyncAPI call arguments.""" + # Must have either operation or channel + if not self.operation and not self.channel: + raise ValueError("Must specify either operation or channel") + # Must have either message or subscription + if not self.message and not self.subscription: + raise ValueError("Must specify either message or subscription") + + +@dataclass +class CallAsyncApiTask(TaskBase): + """AsyncAPI call task.""" + + call: str = field(default="asyncapi", init=False) + with_: CallAsyncApiArguments | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate AsyncAPI task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.with_ is None: + raise ValueError("AsyncAPI call task requires 'with' arguments") + + +# gRPC related classes + + +@dataclass +class GrpcService: + """GRPC service configuration.""" + + name: str + host: str + port: int | None = None + authentication: ReferenceableAuthenticationPolicy | None = None + + +@dataclass +class CallGrpcArguments: + """Arguments for gRPC call.""" + + proto: ExternalResource + service: GrpcService + method: str + arguments: dict[str, Any] | None = None + + +@dataclass +class CallGrpcTask(TaskBase): + """gRPC call task.""" + + call: str = field(default="grpc", init=False) + with_: CallGrpcArguments | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate gRPC task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.with_ is None: + raise ValueError("gRPC call task requires 'with' arguments") + + +# HTTP related classes + + +@dataclass +class CallHttpArguments: + """Arguments for HTTP call.""" + + method: str + endpoint: Endpoint + headers: dict[str, str] | str | None = None # Can be runtime expression + body: Any | None = None + query: dict[str, str] | str | None = None # Can be runtime expression + output: str | None = None # raw, content, or response (defaults to content if not specified) + redirect: bool | None = None + + +@dataclass +class CallHttpTask(TaskBase): + """HTTP call task.""" + + call: str = field(default="http", init=False) + with_: CallHttpArguments | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate HTTP task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.with_ is None: + raise ValueError("HTTP call task requires 'with' arguments") + + +# OpenAPI related classes + + +@dataclass +class CallOpenApiArguments: + """Arguments for OpenAPI call.""" + + document: ExternalResource + operationId: str + parameters: dict[str, Any] | None = None + authentication: ReferenceableAuthenticationPolicy | None = None + output: str | None = None # raw, content, or response + redirect: bool | None = None + + +@dataclass +class CallOpenApiTask(TaskBase): + """OpenAPI call task.""" + + call: str = field(default="openapi", init=False) + with_: CallOpenApiArguments | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate OpenAPI task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.with_ is None: + raise ValueError("OpenAPI call task requires 'with' arguments") + + +# A2A related classes + + +@dataclass +class CallA2AArguments: + """Arguments for A2A call.""" + + method: str + agentCard: ExternalResource | None = None + server: Endpoint | None = None + parameters: dict[str, Any] | str | None = None + + +@dataclass +class CallA2ATask(TaskBase): + """A2A call task.""" + + call: str = field(default="a2a", init=False) + with_: CallA2AArguments | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate A2A task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.with_ is None: + raise ValueError("A2A call task requires 'with' arguments") + + +# MCP related classes + + +@dataclass +class McpHttpTransport: + """HTTP transport for MCP.""" + + endpoint: Endpoint + headers: dict[str, str] | None = None + + +@dataclass +class McpStdioTransport: + """STDIO transport for MCP.""" + + command: str + arguments: list[str] | None = None + environment: dict[str, str] | None = None + + +@dataclass +class McpTransport: + """MCP transport configuration.""" + + http: McpHttpTransport | None = None + stdio: McpStdioTransport | None = None + options: dict[str, str] | None = None + + def __post_init__(self): + """Validate MCP transport configuration.""" + if not self.http and not self.stdio: + raise ValueError("Must specify either http or stdio transport") + if self.http and self.stdio: + raise ValueError("Cannot specify both http and stdio transport") + + +@dataclass +class McpClient: + """MCP client description.""" + + name: str + version: str + + +@dataclass +class CallMcpArguments: + """Arguments for MCP call.""" + + method: str + transport: McpTransport + protocolVersion: str = "2025-06-18" + parameters: dict[str, Any] | str | None = None + timeout: Duration | None = None + client: McpClient | None = None + + +@dataclass +class CallMcpTask(TaskBase): + """MCP call task.""" + + call: str = field(default="mcp", init=False) + with_: CallMcpArguments | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate MCP task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.with_ is None: + raise ValueError("MCP call task requires 'with' arguments") + + +# Function call + + +@dataclass +class CallFunctionTask(TaskBase): + """Function call task.""" + + call: str = "" # The name of the function to call + with_: dict[str, Any] | None = field(default=None, metadata={"alias": "with"}) + + def __post_init__(self): + """Validate function task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + # Ensure it's not one of the reserved call types + reserved = ["asyncapi", "grpc", "http", "openapi", "a2a", "mcp"] + if self.call in reserved: + raise ValueError(f"call value '{self.call}' is reserved for specific call types") + if not self.call: + raise ValueError("call must be specified for function task") + + +# Union type for all call tasks +CallTask = ( + CallAsyncApiTask + | CallGrpcTask + | CallHttpTask + | CallOpenApiTask + | CallA2ATask + | CallMcpTask + | CallFunctionTask +) diff --git a/serverlessworkflow/sdk/callback_state.py b/serverlessworkflow/sdk/callback_state.py deleted file mode 100644 index 1b73efb..0000000 --- a/serverlessworkflow/sdk/callback_state.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.callback_state_timeout import CallbackStateTimeOut -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.event_data_filter import EventDataFilter -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, ArrayTypeOf, HydratableParameter, SimpleTypeOf, \ - UnionTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class CallbackState(State, SwfBase): - id: str = None - name: str = None - type: str = None - action: Action = None - eventRef: str = None - timeouts: CallbackStateTimeOut = None - eventDataFilter: EventDataFilter = None - stateDataFilter: StateDataFilter = None - onErrors: [Error] = None - transition: (str | Transition) = None - end: (bool | End) = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - action: Action = None, - eventRef: str = None, - timeouts: CallbackStateTimeOut = None, - eventDataFilter: EventDataFilter = None, - stateDataFilter: StateDataFilter = None, - onErrors: [Error] = None, - transition: (str | Transition) = None, - end: (bool | End) = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'callback', 'usedForCompensation': False, } - SwfBase.__init__(self, locals(), kwargs, CallbackState.f_hydration, _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'action': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(Action)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(CallbackStateTimeOut)) - - if p_key == 'eventDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(EventDataFilter)) - - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/callback_state_timeout.py b/serverlessworkflow/sdk/callback_state_timeout.py deleted file mode 100644 index 9590965..0000000 --- a/serverlessworkflow/sdk/callback_state_timeout.py +++ /dev/null @@ -1,24 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase - - -class CallbackStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - actionExecTimeOut: str = None # ActionExecTimeOut - eventTimeOut: str = None # EventTimeOut - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - actionExecTimeOut: str = None, - eventTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, CallbackStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/continue_as_def.py b/serverlessworkflow/sdk/continue_as_def.py deleted file mode 100644 index 2c341e8..0000000 --- a/serverlessworkflow/sdk/continue_as_def.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, SimpleTypeOf, UnionTypeOf, HydratableParameter, SwfBase -from serverlessworkflow.sdk.workflow_exec_timeout import WorkflowExecTimeOut - - -class ContinueAsDef(SwfBase): - workflowId: str = None - version: str = None - data: (str | dict) = None - workflowExecTimeOut: WorkflowExecTimeOut = None - - def __init__(self, - workflowId: str = None, - version: str = None, - data: (str | dict) = None, - workflowExecTimeOut: WorkflowExecTimeOut = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, ContinueAsDef.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'data': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(dict)])) - - if p_key == 'workflowExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(WorkflowExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/correlation_def.py b/serverlessworkflow/sdk/correlation_def.py deleted file mode 100644 index b113f7d..0000000 --- a/serverlessworkflow/sdk/correlation_def.py +++ /dev/null @@ -1,12 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class CorrelationDef(SwfBase): - contextAttributeName: str = None - contextAttributeValue: str = None - - def __init__(self, - contextAttributeName: str = None, - contextAttributeValue: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/cron_def.py b/serverlessworkflow/sdk/cron_def.py deleted file mode 100644 index 54f05b6..0000000 --- a/serverlessworkflow/sdk/cron_def.py +++ /dev/null @@ -1,12 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class CronDef(SwfBase): - expression: str = None - validUntil: str = None - - def __init__(self, - expression: str = None, - validUntil: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/databased_switch_state.py b/serverlessworkflow/sdk/databased_switch_state.py deleted file mode 100644 index 72735bc..0000000 --- a/serverlessworkflow/sdk/databased_switch_state.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.databased_switch_state_timeout import DataBasedSwitchStateTime0ut -from serverlessworkflow.sdk.default_condition_def import DefaultConditionDef -from serverlessworkflow.sdk.end_data_condition import EndDataCondition -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, ArrayTypeOf, SwfBase -from serverlessworkflow.sdk.transition_data_condition import TransitionDataCondition - - -class DataBasedSwitchState(State, SwfBase): - id: str = None - name: str = None - type: str = None - stateDataFilter: StateDataFilter = None - timeouts: DataBasedSwitchStateTime0ut = None - dataConditions: ([TransitionDataCondition] | [EndDataCondition]) = None - onErrors: [Error] = None - defaultCondition: DefaultConditionDef = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - stateDataFilter: StateDataFilter = None, - timeouts: DataBasedSwitchStateTime0ut = None, - dataConditions: ([TransitionDataCondition] | [EndDataCondition]) = None, - onErrors: [Error] = None, - defaultCondition: DefaultConditionDef = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'switch', 'usedForCompensation': False} - SwfBase.__init__(self, locals(), kwargs, DataBasedSwitchState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(DataBasedSwitchStateTime0ut)) - - if p_key == 'dataConditions': - return [DataBasedSwitchState.hydrate_state(v) if not ( - isinstance(v, TransitionDataCondition or EndDataCondition)) else v for v in p_value] - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'defaultCondition': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(DefaultConditionDef)) - - return copy.deepcopy(p_value) - - @staticmethod - def hydrate_state(v): - state = State(**v) - if hasattr(state, "transition"): - return TransitionDataCondition(**v) - if hasattr(state, "end"): - return EndDataCondition(**v) - raise Exception(f"Unexpected DataBasedSwitchState value: {v}") diff --git a/serverlessworkflow/sdk/databased_switch_state_timeout.py b/serverlessworkflow/sdk/databased_switch_state_timeout.py deleted file mode 100644 index 7601921..0000000 --- a/serverlessworkflow/sdk/databased_switch_state_timeout.py +++ /dev/null @@ -1,20 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, HydratableParameter, SwfBase - - -class DataBasedSwitchStateTime0ut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, DataBasedSwitchStateTime0ut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/default_condition_def.py b/serverlessworkflow/sdk/default_condition_def.py deleted file mode 100644 index b671d8a..0000000 --- a/serverlessworkflow/sdk/default_condition_def.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.swf_base import UnionTypeOf, SimpleTypeOf, ComplexTypeOf, HydratableParameter, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class DefaultConditionDef(SwfBase): - transition: (str | Transition) = None - end: (bool | End) = None - - def __init__(self, - transition: (str | Transition) = None, - end: (bool | End) = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, DefaultConditionDef.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/draw.py b/serverlessworkflow/sdk/draw.py new file mode 100644 index 0000000..8ebb960 --- /dev/null +++ b/serverlessworkflow/sdk/draw.py @@ -0,0 +1,930 @@ +"""Graph visualization utilities for Serverless Workflow.""" + +import re +from dataclasses import dataclass, field +from functools import singledispatch +from typing import Any + +import pydot + +from serverlessworkflow.sdk import Workflow +from serverlessworkflow.sdk.base import TaskBase, TaskItem +from serverlessworkflow.sdk.call_tasks import CallAsyncApiTask, CallHttpTask +from serverlessworkflow.sdk.tasks import ( + DoTask, + EmitTask, + ForkTask, + ForTask, + ListenTask, + RaiseTask, + RunTask, + SetTask, + SwitchTask, + TryTask, + WaitTask, +) + + +@dataclass +class Node: + """Generic node representation, independent of rendering library.""" + + name: str + label: str | None = None + shape: str = "box" + style: str = "rounded" + fillcolor: str | None = None + color: str | None = None + + def __post_init__(self): + """Set label to name if not provided.""" + if self.label is None: + self.label = self.name + + def to_pydot(self) -> pydot.Node: + """Convert to pydot.Node for rendering.""" + attrs: dict[str, Any] = {"label": f'"{self.label}"'} + if self.shape: + attrs["shape"] = self.shape + if self.style: + attrs["style"] = self.style + if self.fillcolor: + attrs["fillcolor"] = self.fillcolor + if self.color: + attrs["color"] = self.color + return pydot.Node(self.name, **attrs) + + +@dataclass +class Edge: + """Generic edge representation, independent of rendering library.""" + + source: str + destination: str + label: str | None = None + style: str | None = None + color: str | None = None + xlabel: str | None = None + + def __hash__(self): + """Return hash of edge attributes.""" + return hash( + (self.source, self.destination, self.label, self.style, self.color, self.xlabel) + ) + + def __eq__(self, other): + """Check equality with another edge.""" + if not isinstance(other, Edge): + return False + return ( + self.source == other.source + and self.destination == other.destination + and self.label == other.label + and self.style == other.style + and self.color == other.color + and self.xlabel == other.xlabel + ) + + def to_pydot(self) -> pydot.Edge: + """Convert to pydot.Edge for rendering.""" + attrs: dict[str, Any] = {} + if self.label: + attrs["label"] = f'"{self.label}"' + if self.style: + attrs["style"] = self.style + if self.color: + attrs["color"] = self.color + if self.xlabel: + attrs["xlabel"] = f'"{self.xlabel}"' + return pydot.Edge(self.source, self.destination, **attrs) + + +@dataclass +class Cluster: + """Generic cluster/subgraph representation, independent of rendering library.""" + + id: str + label: str | None = None + style: str | None = None + color: str | None = None + fillcolor: str | None = None + gradientangle: str | None = None + labelloc: str = "t" + nodes: list[Node] = field(default_factory=list) + edges: list[Edge] = field(default_factory=list) + subclusters: list["Cluster"] = field(default_factory=list) + + def to_pydot(self) -> pydot.Cluster: + """Convert to pydot.Cluster for rendering.""" + attrs: dict[str, Any] = {} + if self.label: + attrs["label"] = f'"{self.label}"' + if self.style: + attrs["style"] = self.style + if self.color: + attrs["color"] = self.color + if self.fillcolor: + attrs["fillcolor"] = self.fillcolor + if self.gradientangle: + attrs["gradientangle"] = self.gradientangle + if self.labelloc: + attrs["labelloc"] = self.labelloc + + cluster = pydot.Cluster(self.id, **attrs) + + # Sort for deterministic output + sorted_nodes = sorted(self.nodes, key=lambda n: n.name) + for node in sorted_nodes: + cluster.add_node(node.to_pydot()) + + sorted_edges = sorted( + self.edges, + key=lambda e: (e.source, e.destination, e.label or "", e.style or "", e.color or ""), + ) + for edge in sorted_edges: + cluster.add_edge(edge.to_pydot()) + + sorted_subclusters = sorted(self.subclusters, key=lambda c: c.id) + for subcluster in sorted_subclusters: + cluster.add_subgraph(subcluster.to_pydot()) # type: ignore[arg-type] + + return cluster + + +def render_workflow_graph( + workflow: Workflow, filename: str | None = None, engine: str = "graphviz" +) -> str: + """Render the workflow state machine to a file. + + :param workflow: The workflow object. + :param title: Title for the graph. + :param filename: Output filename. + :param engine: Graph engine to use ("graphviz"). + """ + if engine == "graphviz": + # Parse the workflow into generic graph structures + nodes: list[Node] = [] + edges: list[Edge] = [] + clusters: list[Cluster] = [] + task_names: set[str] = set() + task_order: list[str] = [] # Track order of tasks for flow edges + inputs: set[str] = set() + input_edges: list[tuple] = [] # (input_name, task_name) - use list to preserve order + + startNode = Node("start", shape="circle", style="filled", fillcolor="lightgreen") + nodes.append(startNode) + + _parse_task( + workflow.do, nodes, edges, clusters, task_names, task_order, inputs, input_edges + ) + + # Add input nodes for inputs that are not also task names + # Sort for deterministic output + pure_inputs = sorted(inputs - task_names) + for input_name in pure_inputs: + input_node = Node( + input_name, label=f"Input: {input_name}", style="filled", fillcolor="lightyellow" + ) + nodes.append(input_node) + + # Add input edges (dashed) + for input_name, task_name in input_edges: + if input_name in pure_inputs: + edges.append(Edge(input_name, task_name, style="dashed", xlabel="input")) + + edges.append(Edge(startNode.name, nodes[1].name)) + + endNode = Node("end", shape="doublecircle", style="filled", fillcolor="lightcoral") + nodes.append(endNode) + + # Connect the last task(s) to the end node + # Use task_order to get the actual last tasks (which may be inside clusters) + if task_order: + # Get all tasks that appear at the end of task_order + # For forks, this will be multiple branch endings + # For sequential tasks, this will be one task + last_tasks = [] + if len(task_order) > 1: + # Check if we have multiple tasks that aren't connected to anything after them + # This happens with fork branches + # Use list to maintain order (deterministic) + potential_last = task_order[-3:] if len(task_order) >= 3 else task_order + # Find tasks that don't have outgoing edges to other tasks in task_order + for task in potential_last: + has_outgoing = False + for edge in edges: + if edge.source == task and edge.destination in task_names: + has_outgoing = True + break + if not has_outgoing or task == task_order[-1]: + last_tasks.append(task) + else: + last_tasks = [task_order[-1]] + + # Remove duplicates while preserving order + seen = set() + unique_last_tasks = [] + for task in last_tasks: + if task not in seen: + seen.add(task) + unique_last_tasks.append(task) + + # Connect each last task to the end node + for last_task in unique_last_tasks: + edges.append(Edge(last_task, endNode.name)) + + # Convert to pydot graph + + graph = pydot.Dot( + workflow.document.name, + graph_type="digraph", + labelloc="top", + fontsize="20", + rankdir="TB", + splines="ortho", + ) + + graph.set_node_defaults(shape="box", style="rounded") + + # Sort for deterministic output + # Nodes are sorted by name + sorted_nodes = sorted(nodes, key=lambda n: n.name) + for node in sorted_nodes: + graph.add_node(node.to_pydot()) + + # Edges are sorted by (source, destination, label) + sorted_edges = sorted( + edges, + key=lambda e: (e.source, e.destination, e.label or "", e.style or "", e.color or ""), + ) + for edge in sorted_edges: + graph.add_edge(edge.to_pydot()) + + # Clusters are sorted by id + sorted_clusters = sorted(clusters, key=lambda c: c.id) + for cluster in sorted_clusters: + graph.add_subgraph(cluster.to_pydot()) # type: ignore[arg-type] + + if filename is not None and filename != "": + graph.write(filename, format="raw") + + return graph.to_string() + + +# Type-specific task handlers using singledispatch +@singledispatch +def _handle_task_object( + _task_obj: Any, + task_name: str, + nodes: list[Node], + edges: list[Edge], + _clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + _inputs: set[str], + _input_edges: list[tuple], + _cluster_counter: list[int], +) -> None: + """Default handler for unknown task types.""" + # For unknown task types, create a simple node + task_label = f"{task_name}" + node = Node(task_name, label=task_label) + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + +@_handle_task_object.register +def _( + task_obj: ForTask, + task_name: str, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + cluster_counter: list[int], +) -> None: + """Handle ForTask: loop iteration.""" + cluster_counter[0] += 1 + + cluster = Cluster(id=f"cluster_{cluster_counter[0]}", style="dashed", labelloc="b") + + # Create a node for the for task itself + for_label = f"{task_name}\\nfor loop" + node = Node(task_name, label=for_label, shape="hexagon", style="filled", fillcolor="lightblue") + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this for task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Parse the tasks within the for loop into the cluster + if task_obj.do: + cluster_nodes: list[Node] = [] + cluster_edges: list[Edge] = [] + cluster_subclusters: list[Cluster] = [] + cluster_task_order: list[str] = [] + + _parse_task( + task_obj.do, + cluster_nodes, + cluster_edges, + cluster_subclusters, + task_names, + cluster_task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + cluster.nodes.extend(cluster_nodes) + cluster.edges.extend(cluster_edges) + cluster.subclusters.extend(cluster_subclusters) + + # Add edge from for task to first task in cluster + if cluster_nodes: + edges.append(Edge(task_name, cluster_nodes[0].name)) + # Add edge from last task in cluster back to for task (loop) + edges.append(Edge(cluster_nodes[-1].name, task_name, style="dotted", label="loop")) + task_order.append(cluster_nodes[-1].name) + + clusters.append(cluster) + + +@_handle_task_object.register +def _( + task_obj: ForkTask, + task_name: str, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + cluster_counter: list[int], +) -> None: + """Handle ForkTask: parallel execution.""" + cluster_counter[0] += 1 + + cluster = Cluster(id=f"cluster_{cluster_counter[0]}", style="dashed", labelloc="b") + + # Create a node for the fork task itself + fork_label = f"{task_name}\\nfork" + node = Node( + task_name, label=fork_label, shape="diamond", style="filled", fillcolor="lightgreen" + ) + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this fork task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Parse the branches within the fork into the cluster + if task_obj.fork: + branches = ( + task_obj.fork.get("branches", []) + if isinstance(task_obj.fork, dict) + else getattr(task_obj.fork, "branches", []) + ) + + branch_last_nodes = [] + for branch in branches: + branch_nodes: list[Node] = [] + branch_edges: list[Edge] = [] + branch_subclusters: list[Cluster] = [] + branch_task_order: list[str] = [] + + _parse_task( + branch, + branch_nodes, + branch_edges, + branch_subclusters, + task_names, + branch_task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + cluster.nodes.extend(branch_nodes) + cluster.edges.extend(branch_edges) + cluster.subclusters.extend(branch_subclusters) + + # Add edge from fork task to first task in each branch + if branch_nodes: + edges.append(Edge(task_name, branch_nodes[0].name)) + branch_last_nodes.append(branch_nodes[-1].name) + + # Update task_order to continue after all branches + if branch_last_nodes: + task_order.extend(branch_last_nodes) + + clusters.append(cluster) + + +@_handle_task_object.register +def _( + task_obj: SwitchTask, + task_name: str, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + _inputs: set[str], + _input_edges: list[tuple], + cluster_counter: list[int], +) -> None: + """Handle SwitchTask: conditional branching.""" + cluster_counter[0] += 1 + + cluster = Cluster(id=f"cluster_{cluster_counter[0]}", style="dashed", labelloc="b") + + # Create a node for the switch task itself + switch_label = f"{task_name}\\nswitch" + node = Node( + task_name, label=switch_label, shape="triangle", style="filled", fillcolor="lightyellow" + ) + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this switch task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Parse the cases within the switch into the cluster + if task_obj.switch: + case_nodes = [] + for case_dict in task_obj.switch: + # Each case_dict is like {'case1': {'when': '...', 'then': '...'}} + for case_name, case_config in case_dict.items(): + # Create a node for each case + when_value = ( + case_config.get("when", "") + if isinstance(case_config, dict) + else getattr(case_config, "when", "") + ) + + # Escape double quotes in the when value for DOT syntax + when_escaped = when_value.replace('"', '\\"') if when_value else "" + + case_label = ( + f"{case_name}\\n{when_escaped}" if when_escaped else f"{case_name}\\ndefault" + ) + case_node = Node( + case_name, + label=case_label, + shape="box", + style="rounded,filled", + fillcolor="lightyellow", + ) + cluster.nodes.append(case_node) + case_nodes.append(case_name) + + # Add edge from switch task to this case + edges.append(Edge(task_name, case_name, label=case_name)) + + # Update task_order to include all case nodes + if case_nodes: + task_order.extend(case_nodes) + + clusters.append(cluster) + + +@_handle_task_object.register +def _( + task_obj: TryTask, + task_name: str, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + cluster_counter: list[int], +) -> None: + """Handle TryTask: error handling with try/catch blocks.""" + cluster_counter[0] += 1 + + main_cluster = Cluster(id=f"cluster_{cluster_counter[0]}", labelloc="b") + + # Create a node for the try-catch task itself + try_label = f"{task_name}\\ntry-catch" + node = Node(task_name, label=try_label, shape="octagon", style="filled", fillcolor="lightpink") + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this try-catch task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Create subcluster for try block + cluster_counter[0] += 1 + try_cluster = Cluster(id=f"cluster_{cluster_counter[0]}", label="try", labelloc="b") + + # Parse the try block + try_nodes = [] + if task_obj.try_: + try_node_list: list[Node] = [] + try_edge_list: list[Edge] = [] + try_subcluster_list: list[Cluster] = [] + try_task_order: list[str] = [] + + _parse_task( + task_obj.try_, + try_node_list, + try_edge_list, + try_subcluster_list, + task_names, + try_task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + try_cluster.nodes.extend(try_node_list) + try_cluster.edges.extend(try_edge_list) + try_cluster.subclusters.extend(try_subcluster_list) + + # Add edge from try-catch task to first task in try block + if try_node_list: + edges.append(Edge(task_name, try_node_list[0].name, label="try")) + try_nodes = try_node_list + + main_cluster.subclusters.append(try_cluster) + + # Create subcluster for catch block + cluster_counter[0] += 1 + catch_cluster = Cluster(id=f"cluster_{cluster_counter[0]}", label="catch", labelloc="b") + + # Parse the catch block + catch_nodes = [] + if task_obj.catch: + catch_config = ( + task_obj.catch if isinstance(task_obj.catch, dict) else task_obj.catch.__dict__ + ) + catch_do = ( + catch_config.get("do", []) + if isinstance(catch_config, dict) + else getattr(task_obj.catch, "do", []) + ) + + if catch_do: + catch_node_list: list[Node] = [] + catch_edge_list: list[Edge] = [] + catch_subcluster_list: list[Cluster] = [] + catch_task_order: list[str] = [] + + _parse_task( + catch_do, + catch_node_list, + catch_edge_list, + catch_subcluster_list, + task_names, + catch_task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + catch_cluster.nodes.extend(catch_node_list) + catch_cluster.edges.extend(catch_edge_list) + catch_cluster.subclusters.extend(catch_subcluster_list) + + # Add edge from try-catch task to first task in catch block (on error) + if catch_node_list: + edges.append( + Edge( + task_name, + catch_node_list[0].name, + label="catch", + style="dashed", + color="red", + ) + ) + catch_nodes = catch_node_list + + main_cluster.subclusters.append(catch_cluster) + + # Update task_order to include both try and catch last nodes + if try_nodes: + task_order.append(try_nodes[-1].name) + if catch_nodes: + task_order.append(catch_nodes[-1].name) + + clusters.append(main_cluster) + + +@_handle_task_object.register +def _( + task_obj: CallHttpTask, + task_name: str, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + cluster_counter: list[int], +) -> None: + """Handle CallHttpTask: HTTP requests.""" + if task_obj.with_ is None: + task_label = f"{task_name}\\ncall: http" + else: + task_label = ( + f"{task_name}\\ncall: http\\n{task_obj.with_.method.upper()} {task_obj.with_.endpoint}" + ) + node = Node(task_name, label=task_label) + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Recursively parse the task object, passing task_name as current + _parse_task( + task_obj, + nodes, + edges, + clusters, + task_names, + task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + +@_handle_task_object.register +def _( + task_obj: CallAsyncApiTask, + task_name: str, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + cluster_counter: list[int], +) -> None: + """Handle CallAsyncApiTask: AsyncAPI operations with optional foreach in subscription.""" + task_label = f"{task_name}\\ncall: asyncapi" + if ( + task_obj.with_ is not None + and hasattr(task_obj.with_, "operation") + and task_obj.with_.operation + ): + task_label += f"\\n{task_obj.with_.operation}" + + node = Node(task_name, label=task_label) + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Check if there's a foreach in the subscription + if ( + task_obj.with_ is not None + and hasattr(task_obj.with_, "subscription") + and task_obj.with_.subscription + ): + subscription = task_obj.with_.subscription + subscription_dict = ( + subscription + if isinstance(subscription, dict) + else (subscription.__dict__ if hasattr(subscription, "__dict__") else {}) + ) + + foreach_config = ( + subscription_dict.get("foreach") + if isinstance(subscription_dict, dict) + else getattr(subscription, "foreach", None) + ) + + if foreach_config: + # Create a cluster for the foreach loop within this task + cluster_counter[0] += 1 + + cluster = Cluster(id=f"cluster_{cluster_counter[0]}", style="dashed", labelloc="b") + + # Parse the tasks within the foreach + foreach_do = ( + foreach_config.get("do", []) + if isinstance(foreach_config, dict) + else getattr(foreach_config, "do", []) + ) + + if foreach_do: + foreach_nodes: list[Node] = [] + foreach_edges: list[Edge] = [] + foreach_subclusters: list[Cluster] = [] + foreach_task_order: list[str] = [] + + _parse_task( + foreach_do, + foreach_nodes, + foreach_edges, + foreach_subclusters, + task_names, + foreach_task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + cluster.nodes.extend(foreach_nodes) + cluster.edges.extend(foreach_edges) + cluster.subclusters.extend(foreach_subclusters) + + # Add edge from main task to first task in foreach + if foreach_nodes: + edges.append(Edge(task_name, foreach_nodes[0].name, label="foreach")) + # Update task_order to include the last task in the foreach + task_order.append(foreach_nodes[-1].name) + + clusters.append(cluster) + + # Recursively parse the task object for other nested structures + _parse_task( + task_obj, + nodes, + edges, + clusters, + task_names, + task_order, + inputs, + input_edges, + task_name, + cluster_counter, + ) + + +@_handle_task_object.register(ListenTask) +@_handle_task_object.register(SetTask) +@_handle_task_object.register(WaitTask) +@_handle_task_object.register(EmitTask) +@_handle_task_object.register(RaiseTask) +@_handle_task_object.register(RunTask) +@_handle_task_object.register(DoTask) +def _( + task_obj: Any, + task_name: str, + nodes: list[Node], + edges: list[Edge], + _clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + _cluster_counter: list[int], +) -> None: + """Handle simple task types with basic rendering.""" + task_type = type(task_obj).__name__.replace("Task", "").lower() + task_label = f"{task_name}\\n{task_type}" + node = Node(task_name, label=task_label) + nodes.append(node) + task_names.add(task_name) + + # Create edge from previous task to this task + if task_order: + prev_task = task_order[-1] + edges.append(Edge(prev_task, task_name)) + task_order.append(task_name) + + # Extract inputs from task + _extract_inputs(task_obj, inputs, input_edges, task_name) + + +def _parse_task( + task: list[TaskItem] | TaskItem | dict | CallHttpTask | CallAsyncApiTask, + nodes: list[Node], + edges: list[Edge], + clusters: list[Cluster], + task_names: set[str], + task_order: list[str], + inputs: set[str], + input_edges: list[tuple], + current_task_name: str | None = None, + cluster_counter: list[int] | None = None, +) -> None: + """Parse workflow tasks into generic graph structures.""" + if cluster_counter is None: + cluster_counter = [0] + + if isinstance(task, list): + for t in task: + _parse_task( + t, + nodes, + edges, + clusters, + task_names, + task_order, + inputs, + input_edges, + current_task_name, + cluster_counter, + ) + elif isinstance(task, TaskItem): + # Handle TaskItem objects (name + task pair) + _handle_task_object( + task.task, + task.name, + nodes, + edges, + clusters, + task_names, + task_order, + inputs, + input_edges, + cluster_counter, + ) + elif isinstance(task, dict): + for task_name, task_obj in task.items(): + # Dispatch to the appropriate handler based on task type + _handle_task_object( + task_obj, + task_name, + nodes, + edges, + clusters, + task_names, + task_order, + inputs, + input_edges, + cluster_counter, + ) + elif isinstance(task, TaskBase): + # Extract inputs from task (e.g., {petId} in endpoint URLs) + # and track edges from input to current task + _extract_inputs(task, inputs, input_edges, current_task_name) + # Handle nested tasks within TaskBase objects (e.g., do, fork, etc.) + if ( + hasattr(task, "do") + and task.do is not None + and not isinstance(task, (ForTask | ForkTask | SwitchTask)) + ): + _parse_task( + task.do, + nodes, + edges, + clusters, + task_names, + task_order, + inputs, + input_edges, + current_task_name, + cluster_counter, + ) + + +def _extract_inputs( + task: TaskBase, inputs: set[str], input_edges: list[tuple], current_task_name: str | None +) -> None: + """Extract input variables from a task and track edges to the task.""" + # Look for {variable} patterns in task properties + # For HTTP tasks, check the endpoint + if hasattr(task, "with_") and task.with_ is not None: + with_obj = task.with_ + if hasattr(with_obj, "endpoint") and with_obj.endpoint: + # Find all {variable} patterns + matches = re.findall(r"\{(\w+)\}", str(with_obj.endpoint)) + for match in matches: + inputs.add(match) + if current_task_name and (match, current_task_name) not in input_edges: + input_edges.append((match, current_task_name)) diff --git a/serverlessworkflow/sdk/end.py b/serverlessworkflow/sdk/end.py deleted file mode 100644 index 02fc750..0000000 --- a/serverlessworkflow/sdk/end.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.continue_as_def import ContinueAsDef -from serverlessworkflow.sdk.produce_event_def import ProduceEventDef -from serverlessworkflow.sdk.swf_base import HydratableParameter, ArrayTypeOf, UnionTypeOf, SimpleTypeOf, \ - ComplexTypeOf, SwfBase - - -class End(SwfBase): - terminate: bool = None - produceEvents: [ProduceEventDef] = None - compensate: bool = None - continueAs: (str | ContinueAsDef) = None - - def __init__(self, - terminate: bool = None, - produceEvents: [ProduceEventDef] = None, - compensate: bool = None, - continueAs: (str | ContinueAsDef) = None, - **kwargs): - - _default_values = {'compensate': False, 'terminate': False, } - SwfBase.__init__(self, locals(), kwargs, End.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'produceEvents': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(ProduceEventDef)) - - if p_key == 'continueAs': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(ContinueAsDef)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/end_data_condition.py b/serverlessworkflow/sdk/end_data_condition.py deleted file mode 100644 index d8974d7..0000000 --- a/serverlessworkflow/sdk/end_data_condition.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import SimpleTypeOf, ComplexTypeOf, UnionTypeOf, HydratableParameter, SwfBase - - -class EndDataCondition(SwfBase): - name: str = None - condition: str = None - end: (bool | End) = None - metadata: Metadata = None - - def __init__(self, - name: str = None, - condition: str = None, - end: (bool | End) = None, - metadata: Metadata = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, EndDataCondition.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/end_event_condition.py b/serverlessworkflow/sdk/end_event_condition.py deleted file mode 100644 index f449298..0000000 --- a/serverlessworkflow/sdk/end_event_condition.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.event_data_filter import EventDataFilter -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, ComplexTypeOf, SimpleTypeOf, SwfBase - - -class EndEventCondition(SwfBase): - name: str = None - eventRef: str = None - end: (bool | End) = None - eventDataFilter: EventDataFilter = None - metadata: Metadata = None - - def __init__(self, - name: str = None, - eventRef: str = None, - end: (bool | End) = None, - eventDataFilter: EventDataFilter = None, - metadata: Metadata = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, EndEventCondition.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - - if p_key == 'eventDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(EventDataFilter)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/endpoint.py b/serverlessworkflow/sdk/endpoint.py new file mode 100644 index 0000000..32a2e91 --- /dev/null +++ b/serverlessworkflow/sdk/endpoint.py @@ -0,0 +1,33 @@ +"""Endpoint and related classes for Serverless Workflow SDK v1.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from serverlessworkflow.sdk.authentication import ReferenceableAuthenticationPolicy + +from serverlessworkflow.sdk.base import RuntimeExpression, UriTemplate + + +@dataclass +class Endpoint: + """Represents an endpoint - can be a URI, expression, or configuration object.""" + + uri: UriTemplate | RuntimeExpression | None = None + authentication: ReferenceableAuthenticationPolicy | None = None + + def __post_init__(self): + """Validate endpoint configuration.""" + # If only uri is set and authentication is None, it's a simple endpoint + # If both are set, it's a full endpoint configuration + if self.uri is None: + raise ValueError("Endpoint must have a uri specified") + + +@dataclass +class Catalog: + """The definition of a resource catalog.""" + + endpoint: Endpoint diff --git a/serverlessworkflow/sdk/error.py b/serverlessworkflow/sdk/error.py deleted file mode 100644 index 85fc594..0000000 --- a/serverlessworkflow/sdk/error.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.swf_base import HydratableParameter, ArrayTypeOf, SimpleTypeOf, ComplexTypeOf, \ - UnionTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class Error(SwfBase): - errorRef: str = None - errorRefs: [str] = None - transition: (str | Transition) = None - end: (bool | End) = None - - def __init__(self, - errorRef: str = None, - errorRefs: [str] = None, - transition: (str | Transition) = None, - end: (bool | End) = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, Error.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'errorRefs': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(ComplexTypeOf(Error))) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/error_def.py b/serverlessworkflow/sdk/error_def.py deleted file mode 100644 index e7c881f..0000000 --- a/serverlessworkflow/sdk/error_def.py +++ /dev/null @@ -1,14 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class ErrorDef(SwfBase): - name: str = None - code: str = None - description: str = None - - def __init__(self, - name: str = None, - code: str = None, - description: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/event_based_switch_state.py b/serverlessworkflow/sdk/event_based_switch_state.py deleted file mode 100644 index 1f55111..0000000 --- a/serverlessworkflow/sdk/event_based_switch_state.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.default_condition_def import DefaultConditionDef -from serverlessworkflow.sdk.end_event_condition import EndEventCondition -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.event_based_switch_state_timeout import EventBasedSwitchStateTimeOut -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, ArrayTypeOf, SwfBase -from serverlessworkflow.sdk.transition_event_condition import TransitionEventCondition - - -class EventBasedSwitchState(State, SwfBase): - id: str = None - name: str = None - type: str = None - stateDataFilter: StateDataFilter = None - timeouts: EventBasedSwitchStateTimeOut = None - eventConditions: ([TransitionEventCondition] | [EndEventCondition]) = None # Eventcondition - onErrors: [Error] = None - defaultCondition: DefaultConditionDef = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - stateDataFilter: StateDataFilter = None, - timeouts: EventBasedSwitchStateTimeOut = None, - eventConditions: ([TransitionEventCondition] | [EndEventCondition]) = None, # Eventcondition - onErrors: [Error] = None, - defaultCondition: DefaultConditionDef = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'switch', 'usedForCompensation': False} - SwfBase.__init__(self, locals(), kwargs, EventBasedSwitchState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(EventBasedSwitchStateTimeOut)) - - if p_key == 'eventConditions': - return [EventBasedSwitchState.hydrate_state(v) if not ( - isinstance(v, TransitionEventCondition or EndEventCondition)) else v for v in p_value] - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'defaultCondition': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(DefaultConditionDef)) - - return copy.deepcopy(p_value) - - @staticmethod - def hydrate_state(event): - state = State(**event) - if hasattr(state, "transition"): - return TransitionEventCondition(**event) - if hasattr(state, "end"): - return EndEventCondition(**event) - raise Exception(f"Unexpected EventBasedSwitchState value: {event}") diff --git a/serverlessworkflow/sdk/event_based_switch_state_timeout.py b/serverlessworkflow/sdk/event_based_switch_state_timeout.py deleted file mode 100644 index 1d7a3e8..0000000 --- a/serverlessworkflow/sdk/event_based_switch_state_timeout.py +++ /dev/null @@ -1,22 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase - - -class EventBasedSwitchStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - eventTimeOut: str = None # EventTimeOut - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - eventTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, EventBasedSwitchStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/event_data_filter.py b/serverlessworkflow/sdk/event_data_filter.py deleted file mode 100644 index ad06ca2..0000000 --- a/serverlessworkflow/sdk/event_data_filter.py +++ /dev/null @@ -1,14 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class EventDataFilter(SwfBase): - useData: bool = None - data: str = None - toStateData: str = None - - def __init__(self, - useData: bool = None, - data: str = None, - toStateData: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/event_def.py b/serverlessworkflow/sdk/event_def.py deleted file mode 100644 index 59364ed..0000000 --- a/serverlessworkflow/sdk/event_def.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import copy -from enum import Enum - -from serverlessworkflow.sdk.correlation_def import CorrelationDef -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, ArrayTypeOf, ComplexTypeOf, SwfBase - - -class Kind(Enum): - CONSUMED = "consumed" - PRODUCED = "produced" - - -class EventDef(SwfBase): - name: str = None - source: str = None - type: str = None - kind: Kind = None - correlation: (CorrelationDef | [CorrelationDef]) = None - dataOnly: bool = None - metadata: Metadata = None - - def __init__(self, - name: str = None, - source: str = None, - type: str = None, - kind: Kind = None, - correlation: (CorrelationDef | [CorrelationDef]) = None, # CorrelationDefs - dataOnly: bool = None, - metadata: Metadata = None, - **kwargs): - _default_values = {'kind': 'consumed', 'dataOnly': True, } - SwfBase.__init__(self, locals(), kwargs, EventDef.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'correlation': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([ComplexTypeOf(CorrelationDef), - ArrayTypeOf(CorrelationDef)])) - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/event_ref.py b/serverlessworkflow/sdk/event_ref.py deleted file mode 100644 index 0b5a3e4..0000000 --- a/serverlessworkflow/sdk/event_ref.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, UnionTypeOf, SimpleTypeOf, SwfBase - - -class EventRef(SwfBase): - triggerEventRef: str = None - resultEventRef: str = None - resultEventTimeOut: str = None - data: (str | dict) = None - contextAttributes: dict[str, str] = None - invoke: str = None - - def __init__(self, - triggerEventRef: str = None, - resultEventRef: str = None, - data: (str | dict) = None, - contextAttributes: dict[str, str] = None, - invoke: str = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, EventRef.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'data': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(dict)])) - - if p_key == 'contextAttributes': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(dict)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/event_state.py b/serverlessworkflow/sdk/event_state.py deleted file mode 100644 index e11c9d7..0000000 --- a/serverlessworkflow/sdk/event_state.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.event_state_timeout import EventStateTimeOut -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.on_events import OnEvents -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import ArrayTypeOf, HydratableParameter, ComplexTypeOf, UnionTypeOf, \ - SimpleTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class EventState(State, SwfBase): - id: str = None - name: str = None - type: str = None - exclusive: bool = None - onEvents: [OnEvents] = None - timeouts: EventStateTimeOut = None - stateDataFilter: StateDataFilter = None - onErrors: [Error] = None - transition: (str | Transition) = None - end: (bool | End) = None - compensatedBy: str = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - exclusive: bool = None, - onEvents: [OnEvents] = None, - timeouts: EventStateTimeOut = None, - stateDataFilter: StateDataFilter = None, - onErrors: [Error] = None, - transition: (str | Transition) = None, - end: (bool | End) = None, - compensatedBy: str = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'event', 'exclusive': True, } - SwfBase.__init__(self, locals(), kwargs, EventState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'onEvents': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(OnEvents)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(EventStateTimeOut)) - - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/event_state_timeout.py b/serverlessworkflow/sdk/event_state_timeout.py deleted file mode 100644 index 15b6e73..0000000 --- a/serverlessworkflow/sdk/event_state_timeout.py +++ /dev/null @@ -1,24 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase - - -class EventStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - actionExecTimeOut: str = None # ActionExecTimeOut - eventTimeOut: str = None # EventTimeOut - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - actionExecTimeOut: str = None, - eventTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, EventStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/events.py b/serverlessworkflow/sdk/events.py new file mode 100644 index 0000000..de0b01b --- /dev/null +++ b/serverlessworkflow/sdk/events.py @@ -0,0 +1,72 @@ +"""Event-related classes for Serverless Workflow SDK v1.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from serverlessworkflow.sdk.base import RuntimeExpression, UriTemplate + + +@dataclass +class EventProperties: + """Describes the properties of an event.""" + + source: UriTemplate | RuntimeExpression + type: str + id: str | None = None + time: str | RuntimeExpression | None = None + subject: str | None = None + datacontenttype: str | None = None + dataschema: UriTemplate | RuntimeExpression | None = None + data: Any | None = None + # Allow additional CloudEvents extension attributes + additional_properties: dict[str, Any] | None = None + + +@dataclass +class Correlation: + """Correlation mapping for event filtering.""" + + from_: str | None = field( + default=None, metadata={"alias": "from"} + ) # Runtime expression to extract correlation value + expect: str | None = None # Expected value or expression + + +@dataclass +class EventFilter: + """Event filter for selective event processing.""" + + with_: EventProperties | None = field( + default=None, metadata={"alias": "with"} + ) # The event properties to match + correlate: dict[str, Correlation] | None = None + + +@dataclass +class EventConsumptionStrategyOne: + """Consume one specific event.""" + + one: EventFilter + + +@dataclass +class EventConsumptionStrategyAny: + """Consume any of the specified events.""" + + any: list[EventFilter] + until: str | EventConsumptionStrategy | None = None + + +@dataclass +class EventConsumptionStrategyAll: + """Consume all specified events.""" + + all: list[EventFilter] + + +# Type alias for event consumption strategy +EventConsumptionStrategy = ( + EventConsumptionStrategyOne | EventConsumptionStrategyAny | EventConsumptionStrategyAll +) diff --git a/serverlessworkflow/sdk/foreach_state.py b/serverlessworkflow/sdk/foreach_state.py deleted file mode 100644 index 5a4eab1..0000000 --- a/serverlessworkflow/sdk/foreach_state.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.foreach_state_timeout import ForEachStateTimeOut -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import SimpleTypeOf, ComplexTypeOf, UnionTypeOf, HydratableParameter, \ - ArrayTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class ForEachState(State, SwfBase): - id: str = None - name: str = None - type: str = None - end: (bool | End) = None - inputCollection: str = None - outputCollection: str = None - iterationParam: str = None - batchSize: (int | str) = None - actions: [Action] = None - timeouts: ForEachStateTimeOut = None - stateDataFilter: StateDataFilter = None - onErrors: [Error] = None - transition: (str | Transition) = None - compensatedBy: str = None - usedForCompensation: bool = None - mode: str = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - end: (bool | End) = None, - inputCollection: str = None, - outputCollection: str = None, - iterationParam: str = None, - batchSize: (int | str) = None, - actions: [Action] = None, - timeouts: ForEachStateTimeOut = None, - stateDataFilter: StateDataFilter = None, - onErrors: [Error] = None, - transition: (str | Transition) = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - mode: str = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'foreach', 'usedForCompensation': False, 'mode': 'parallel', } - SwfBase.__init__(self, locals(), kwargs, ForEachState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - if p_key == 'batchSize': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - SimpleTypeOf(int)])) - - if p_key == 'actions': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Action)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(ForEachStateTimeOut)) - - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/foreach_state_timeout.py b/serverlessworkflow/sdk/foreach_state_timeout.py deleted file mode 100644 index d50a0fc..0000000 --- a/serverlessworkflow/sdk/foreach_state_timeout.py +++ /dev/null @@ -1,22 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase - - -class ForEachStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - actionExecTimeOut: str = None # ActionExecTimeOut - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - actionExecTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, ForEachStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/function.py b/serverlessworkflow/sdk/function.py deleted file mode 100644 index 8e1f47d..0000000 --- a/serverlessworkflow/sdk/function.py +++ /dev/null @@ -1,21 +0,0 @@ -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import SwfBase - - -class Function(SwfBase): - name: str = None - operation: str = None - type: str = None - authRef: str = None - metadata: Metadata = None - - def __init__(self, - name: str = None, - operation: str = None, - type: str = None, - authRef: str = None, - metadata: Metadata = None, - **kwargs): - _default_values = {'type': 'rest'} - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration, - _default_values) diff --git a/serverlessworkflow/sdk/function_ref.py b/serverlessworkflow/sdk/function_ref.py deleted file mode 100644 index eb283db..0000000 --- a/serverlessworkflow/sdk/function_ref.py +++ /dev/null @@ -1,25 +0,0 @@ -import copy - -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, HydratableParameter, SwfBase - - -class FunctionRef(SwfBase): - refName: str = None - arguments: dict[str, dict] = None - selectionSet: str = None - invoke: str = None - - def __init__(self, - refName: str = None, - arguments: dict[str, any] = None, - selectionSet: str = None, - invoke: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, FunctionRef.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'arguments': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(dict)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/inject_state.py b/serverlessworkflow/sdk/inject_state.py deleted file mode 100644 index 7dbfba4..0000000 --- a/serverlessworkflow/sdk/inject_state.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.inject_state_timeout import InjectStateTimeOut -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, SimpleTypeOf, ComplexTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class InjectState(State, SwfBase): - id: str = None - name: str = None - type: str = None - end: (bool | End) = None - data: (str | dict) = None - timeouts: InjectStateTimeOut = None - stateDataFilter: StateDataFilter = None - transition: (str | Transition) = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - end: (bool | End) = None, - data: (str | dict) = None, - timeouts: InjectStateTimeOut = None, - stateDataFilter: StateDataFilter = None, - transition: (str | Transition) = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'inject', 'usedForCompensation': False} - SwfBase.__init__(self, locals(), kwargs, InjectState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - if p_key == 'data': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(dict)])) - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(InjectStateTimeOut)) - - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/inject_state_timeout.py b/serverlessworkflow/sdk/inject_state_timeout.py deleted file mode 100644 index bb08d6e..0000000 --- a/serverlessworkflow/sdk/inject_state_timeout.py +++ /dev/null @@ -1,20 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, HydratableParameter, SwfBase - - -class InjectStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, InjectStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/metadata.py b/serverlessworkflow/sdk/metadata.py deleted file mode 100644 index e9e2902..0000000 --- a/serverlessworkflow/sdk/metadata.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict - -from serverlessworkflow.sdk.swf_base import SwfBase - - -class Metadata(Dict[str, str], SwfBase): - - def __init__(self, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/oauth2props_def.py b/serverlessworkflow/sdk/oauth2props_def.py deleted file mode 100644 index 28c39e3..0000000 --- a/serverlessworkflow/sdk/oauth2props_def.py +++ /dev/null @@ -1,33 +0,0 @@ -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import SwfBase - - -class Oauth2PropsDef(SwfBase): - authority: str = None - grantType: str = None - clientId: str = None - clientSecret: str = None - scopes: [str] = None - username: str = None - password: str = None - audiences: [str] = None - subjectToken: str = None - requestedSubject: str = None - requestedIssuer: str = None - metadata: Metadata = None - - def __init__(self, - authority: str = None, - grantType: str = None, - clientId: str = None, - clientSecret: str = None, - scopes: [str] = None, - username: str = None, - password: str = None, - audiences: [str] = None, - subjectToken: str = None, - requestedSubject: str = None, - requestedIssuer: str = None, - metadata: Metadata = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/on_events.py b/serverlessworkflow/sdk/on_events.py deleted file mode 100644 index 22d5030..0000000 --- a/serverlessworkflow/sdk/on_events.py +++ /dev/null @@ -1,32 +0,0 @@ -import copy - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.event_data_filter import EventDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, ArrayTypeOf, ComplexTypeOf, SwfBase - - -class OnEvents(SwfBase): - eventRefs: [str] = None - actionMode: str = None - actions: [Action] = None - eventDataFilter: EventDataFilter = None - - def __init__(self, - eventRefs: [str] = None, - actionMode: str = None, - actions: [Action] = None, - eventDataFilter: EventDataFilter = None, - **kwargs): - - _default_values = {'actionMode': 'sequential'} - SwfBase.__init__(self, locals(), kwargs, OnEvents.f_hydration, _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'actions': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Action)) - - if p_key == 'eventDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(EventDataFilter)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/operation_state.py b/serverlessworkflow/sdk/operation_state.py deleted file mode 100644 index 224daaf..0000000 --- a/serverlessworkflow/sdk/operation_state.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.operation_state_timeout import OperationStateTimeOut -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, ArrayTypeOf, UnionTypeOf, \ - SimpleTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class OperationState(State, SwfBase): - id: str = None - name: str = None - type: str = None - end: (bool | End) = None - stateDataFilter: StateDataFilter = None - actionMode: str = None - actions: [Action] = None - timeouts: OperationStateTimeOut = None - onErrors: [Error] = None - transition: (str | Transition) = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - stateDataFilter: StateDataFilter = None, - actionMode: str = None, - actions: [Action] = None, - timeouts: OperationStateTimeOut = None, - onErrors: [Error] = None, - transition: (str | Transition) = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - end: (bool | End) = None, - **kwargs): - - _default_values = {'type': 'operation', 'actionMode': 'sequential', 'usedForCompensation': False} - SwfBase.__init__(self, locals(), kwargs, OperationState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'actions': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Action)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(OperationStateTimeOut)) - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs( - UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(Transition)])) - - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), ComplexTypeOf(End)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/operation_state_timeout.py b/serverlessworkflow/sdk/operation_state_timeout.py deleted file mode 100644 index 2f8a6cd..0000000 --- a/serverlessworkflow/sdk/operation_state_timeout.py +++ /dev/null @@ -1,22 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import ComplexTypeOf, HydratableParameter, SwfBase - - -class OperationStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - actionExecTimeOut: str = None # ActionExecTimeOut - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - actionExecTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, OperationStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/parallel_state.py b/serverlessworkflow/sdk/parallel_state.py deleted file mode 100644 index 1f72c5c..0000000 --- a/serverlessworkflow/sdk/parallel_state.py +++ /dev/null @@ -1,82 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.branch import Branch -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.parallel_state_timeout import ParallelStateTimeOut -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, SimpleTypeOf, ComplexTypeOf, \ - ArrayTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class ParallelState(State, SwfBase): - id: str = None - name: str = None - type: str = None - end: (bool | End) = None - stateDataFilter: StateDataFilter = None - timeouts: ParallelStateTimeOut = None - branches: [Branch] = None - completionType: str = None - numCompleted: (int | str) = None - onErrors: [Error] = None - transition: (str | Transition) = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - end: (bool | End) = None, - stateDataFilter: StateDataFilter = None, - timeouts: ParallelStateTimeOut = None, - branches: [Branch] = None, - completionType: str = None, - numCompleted: (int | str) = None, - onErrors: [Error] = None, - transition: (str | Transition) = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'parallel', 'completionType': 'allOf', 'usedForCompensation': False} - SwfBase.__init__(self, locals(), kwargs, ParallelState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(ParallelStateTimeOut)) - - if p_key == 'branches': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Branch)) - - if p_key == 'branches': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Branch)) - - if p_key == 'numCompleted': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(int), - SimpleTypeOf(str)])) - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/parallel_state_timeout.py b/serverlessworkflow/sdk/parallel_state_timeout.py deleted file mode 100644 index 3b736b7..0000000 --- a/serverlessworkflow/sdk/parallel_state_timeout.py +++ /dev/null @@ -1,22 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase - - -class ParallelStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - branchExecTimeOut: str = None # BranchExecTimeOut - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - branchExecTimeOut: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, ParallelStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/produce_event_def.py b/serverlessworkflow/sdk/produce_event_def.py deleted file mode 100644 index 31213c4..0000000 --- a/serverlessworkflow/sdk/produce_event_def.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SimpleTypeOf, UnionTypeOf, SwfBase - - -class ProduceEventDef(SwfBase): - eventRef: str = None - data: (str | dict) = None - contextAttributes: dict[str, str] = None - - def __init__(self, - eventRef: str = None, - data: (str | dict) = None, - contextAttributes: dict[str, str] = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, ProduceEventDef.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'data': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(dict)])) - - if p_key == 'contextAttributes': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(dict)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/retry.py b/serverlessworkflow/sdk/retry.py new file mode 100644 index 0000000..67db691 --- /dev/null +++ b/serverlessworkflow/sdk/retry.py @@ -0,0 +1,62 @@ +"""Retry policy classes for Serverless Workflow SDK v1.""" + +from dataclasses import dataclass, field + +from serverlessworkflow.sdk.base import Duration + + +@dataclass +class ConstantBackoff: + """Constant backoff configuration.""" + + constant: dict # Empty object as marker + + +@dataclass +class ExponentialBackoff: + """Exponential backoff configuration.""" + + exponential: dict # Empty object as marker + + +@dataclass +class LinearBackoff: + """Linear backoff configuration.""" + + linear: dict # Empty object as marker + + +@dataclass +class RetryLimitAttempt: + """Retry attempt limit configuration.""" + + count: int | None = None + duration: Duration | None = None + + +@dataclass +class RetryLimit: + """Retry limits configuration.""" + + attempt: RetryLimitAttempt | None = None + duration: Duration | None = None + + +@dataclass +class RetryJitter: + """Jitter configuration for retry delays.""" + + from_: Duration = field(metadata={"alias": "from"}) + to: Duration + + +@dataclass +class RetryPolicy: + """Defines a retry policy.""" + + when: str | None = None # Runtime expression + exceptWhen: str | None = None # Runtime expression + delay: Duration | None = None + backoff: ConstantBackoff | ExponentialBackoff | LinearBackoff | None = None + limit: RetryLimit | None = None + jitter: RetryJitter | None = None diff --git a/serverlessworkflow/sdk/retry_def.py b/serverlessworkflow/sdk/retry_def.py deleted file mode 100644 index 8cc277d..0000000 --- a/serverlessworkflow/sdk/retry_def.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from serverlessworkflow.sdk.swf_base import SwfBase - - -class RetryDef(SwfBase): - name: str = None - delay: str = None - maxDelay: str = None - increment: str = None - multiplier: (int | str) = None - maxAttempts: (int | str) = None - jitter: (int | str) = None - - def __init__(self, - name: str = None, - delay: str = None, - maxDelay: str = None, - increment: str = None, - multiplier: (int | str) = None, - maxAttempts: (int | str) = None, - jitter: (int | str) = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/schedule.py b/serverlessworkflow/sdk/schedule.py deleted file mode 100644 index 29669e4..0000000 --- a/serverlessworkflow/sdk/schedule.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.cron_def import CronDef -from serverlessworkflow.sdk.swf_base import HydratableParameter, SimpleTypeOf, ComplexTypeOf, UnionTypeOf, SwfBase - - -class Schedule(SwfBase): - interval: str = None - cron: (str | CronDef) = None - timezone: str = None - - def __init__(self, - interval: str = None, - cron: (str | CronDef) = None, - timezone: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, Schedule.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'cron': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(CronDef)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/serializable.py b/serverlessworkflow/sdk/serializable.py deleted file mode 100644 index 8b13789..0000000 --- a/serverlessworkflow/sdk/serializable.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/serverlessworkflow/sdk/sleep.py b/serverlessworkflow/sdk/sleep.py deleted file mode 100644 index 53e0e92..0000000 --- a/serverlessworkflow/sdk/sleep.py +++ /dev/null @@ -1,12 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class Sleep(SwfBase): - before: str = None - after: str = None - - def __init__(self, - before: str = None, - after: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/sleep_state.py b/serverlessworkflow/sdk/sleep_state.py deleted file mode 100644 index 14ece00..0000000 --- a/serverlessworkflow/sdk/sleep_state.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.end import End -from serverlessworkflow.sdk.error import Error -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.sleep_state_timeout import SleepStateTimeOut -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.state_data_filter import StateDataFilter -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, UnionTypeOf, SimpleTypeOf, \ - ArrayTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class SleepState(State, SwfBase): - id: str = None - name: str = None - type: str = None - end: (bool | End) = None - stateDataFilter: StateDataFilter = None - duration: str = None - timeouts: SleepStateTimeOut = None - onErrors: [Error] = None - transition: (str | Transition) = None - compensatedBy: str = None - usedForCompensation: bool = None - metadata: Metadata = None - - def __init__(self, - id: str = None, - name: str = None, - type: str = None, - end: (bool | End) = None, - stateDataFilter: StateDataFilter = None, - duration: str = None, - timeouts: SleepStateTimeOut = None, - onErrors: [Error] = None, - transition: (str | Transition) = None, - compensatedBy: str = None, - usedForCompensation: bool = None, - metadata: Metadata = None, - **kwargs): - - _default_values = {'type': 'sleep', 'usedForCompensation': False} - SwfBase.__init__(self, locals(), kwargs, SleepState.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'end': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(bool), - ComplexTypeOf(End)])) - - if p_key == 'stateDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateDataFilter)) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(SleepStateTimeOut)) - - if p_key == 'onErrors': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(Error)) - - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/sleep_state_timeout.py b/serverlessworkflow/sdk/sleep_state_timeout.py deleted file mode 100644 index a69d749..0000000 --- a/serverlessworkflow/sdk/sleep_state_timeout.py +++ /dev/null @@ -1,16 +0,0 @@ -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase - - -class SleepStateTimeOut(SwfBase): - stateExecTimeOut: StateExecTimeOut = None - - def __init__(self, - stateExecTimeOut: StateExecTimeOut = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SleepStateTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) diff --git a/serverlessworkflow/sdk/start_def.py b/serverlessworkflow/sdk/start_def.py deleted file mode 100644 index 87d7b74..0000000 --- a/serverlessworkflow/sdk/start_def.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.schedule import Schedule -from serverlessworkflow.sdk.swf_base import SimpleTypeOf, ComplexTypeOf, UnionTypeOf, HydratableParameter, SwfBase - - -class StartDef(SwfBase): - stateName: str = None - schedule: (str | Schedule) = None - - def __init__(self, - stateName: str = None, - schedule: (str | Schedule) = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, StartDef.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'schedule': - return HydratableParameter(value=p_value).hydrateAs( - UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(Schedule)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/state.py b/serverlessworkflow/sdk/state.py deleted file mode 100644 index 8bb1bb1..0000000 --- a/serverlessworkflow/sdk/state.py +++ /dev/null @@ -1,34 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class State(SwfBase): - type: str = None - - def __init__(self, - type: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) - - def is_event_state(self): - return self.type == 'event' - - def is_operation_state(self): - return self.type == 'operation' - - def is_switch_state(self): - return self.type == 'switch' - - def is_sleep_state(self): - return self.type == 'sleep' - - def is_parallel_state(self): - return self.type == 'parallel' - - def is_inject_state(self): - return self.type == 'inject' - - def is_foreach_state(self): - return self.type == 'foreach' - - def is_callback_state(self): - return self.type == 'callback' diff --git a/serverlessworkflow/sdk/state_data_filter.py b/serverlessworkflow/sdk/state_data_filter.py deleted file mode 100644 index 24a6971..0000000 --- a/serverlessworkflow/sdk/state_data_filter.py +++ /dev/null @@ -1,12 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class StateDataFilter(SwfBase): - input: str = None - output: str = None - - def __init__(self, - input: str = None, - output: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/state_exec_timeout.py b/serverlessworkflow/sdk/state_exec_timeout.py deleted file mode 100644 index 9053794..0000000 --- a/serverlessworkflow/sdk/state_exec_timeout.py +++ /dev/null @@ -1,12 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class StateExecTimeOut(SwfBase): - single: str = None - total: str = None - - def __init__(self, - single: str = None, - total: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/state_machine_extensions.py b/serverlessworkflow/sdk/state_machine_extensions.py deleted file mode 100644 index e6a0fff..0000000 --- a/serverlessworkflow/sdk/state_machine_extensions.py +++ /dev/null @@ -1,41 +0,0 @@ -from transitions.extensions.states import add_state_features, Tags, State -from transitions.extensions import ( - HierarchicalMachine, - GraphMachine, - HierarchicalGraphMachine, -) - - -class Metadata(State): - """Allows states to have metadata. - Attributes: - metadata (dict): A dictionary with the state metadata. - """ - - def __init__(self, *args, **kwargs): - """ - Args: - **kwargs: If kwargs contains `metadata`, assign them to the attribute. - """ - self.metadata = kwargs.pop("metadata", None) - super(Metadata, self).__init__(*args, **kwargs) - - def __getattr__(self, key): - if value := self.metadata.get(key) is not None: - return value - return super(Metadata, self).__getattribute__(key) - - -@add_state_features(Tags, Metadata) -class CustomHierarchicalMachine(HierarchicalMachine): - pass - - -@add_state_features(Tags, Metadata) -class CustomHierarchicalGraphMachine(HierarchicalGraphMachine): - pass - - -@add_state_features(Tags, Metadata) -class CustomGraphMachine(GraphMachine): - pass diff --git a/serverlessworkflow/sdk/state_machine_generator.py b/serverlessworkflow/sdk/state_machine_generator.py deleted file mode 100644 index fc864e3..0000000 --- a/serverlessworkflow/sdk/state_machine_generator.py +++ /dev/null @@ -1,619 +0,0 @@ -from typing import Any, Dict, List, Optional, Union -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.function_ref import FunctionRef -from serverlessworkflow.sdk.state_machine_extensions import ( - CustomGraphMachine, - CustomHierarchicalGraphMachine, - CustomHierarchicalMachine, -) -from serverlessworkflow.sdk.transition import Transition -from serverlessworkflow.sdk.workflow import ( - State, - EventState, - SleepState, - CallbackState, - DataBasedSwitchState, - InjectState, - EventBasedSwitchState, - ParallelState, - OperationState, - ForEachState, - Workflow, -) -from serverlessworkflow.sdk.transition_data_condition import TransitionDataCondition -from serverlessworkflow.sdk.end_data_condition import EndDataCondition - -from transitions.extensions.nesting import NestedState -import warnings - -NestedState.separator = "." - - -class StateMachineGenerator: - def __init__( - self, - workflow: Workflow, - state_machine: Union[CustomHierarchicalMachine, CustomGraphMachine], - subflows: List[Workflow] = [], - get_actions=False, - ): - self.workflow = workflow - self.state_machine = state_machine - self.get_actions = get_actions - self.subflows = subflows - - self.is_first_state = False - self.current_state: State = None - - if ( - self.get_actions - and not isinstance(self.state_machine, CustomHierarchicalMachine) - and not isinstance(self.state_machine, CustomHierarchicalGraphMachine) - ): - raise AttributeError( - "The provided state machine must be of the CustomHierarchicalMachine or CustomHierarchicalGraphMachine types." - ) - if not self.get_actions and ( - isinstance(self.state_machine, CustomHierarchicalMachine) - or isinstance(self.state_machine, CustomHierarchicalGraphMachine) - ): - raise AttributeError( - "The provided state machine can not be of the CustomHierarchicalMachine or CustomHierarchicalGraphMachine types." - ) - - def generate(self): - for self.current_state in self.workflow.states: - self.is_first_state = self.workflow.start == self.current_state.name - self.definitions() - self.transitions() - - def transitions(self): - self.start_transition() - self.data_conditions_transitions() - self.event_conditions_transition() - self.error_transitions() - self.natural_transition( - self.current_state.name, - ( - self.current_state.transition - if hasattr(self.current_state, "transition") - else None - ), - ) - self.compensated_by_transition() - self.end_transition() - - def start_transition(self): - if self.is_first_state: - self.state_machine._initial = self.current_state.name - - def data_conditions_transitions(self): - if isinstance(self.current_state, DataBasedSwitchState): - data_conditions = self.current_state.dataConditions - if data_conditions: - state_name = self.current_state.name - for data_condition in data_conditions: - if isinstance(data_condition, TransitionDataCondition): - transition = data_condition.transition - condition = data_condition.condition - self.natural_transition(state_name, transition, condition) - if ( - isinstance(data_condition, EndDataCondition) - and data_condition.end - ): - condition = data_condition.condition - self.end_state(state_name, condition=condition) - self.default_condition_transition(self.current_state) - - def event_conditions_transition(self): - if isinstance(self.current_state, EventBasedSwitchState): - event_conditions = self.current_state.eventConditions - if event_conditions: - state_name = self.current_state.name - for event_condition in event_conditions: - transition = event_condition.transition - event_ref = event_condition.eventRef - self.natural_transition(state_name, transition, event_ref) - if event_condition.end: - self.end_state(state_name, condition=event_ref) - self.default_condition_transition(self.current_state) - - def default_condition_transition(self, state: State): - if hasattr(state, "defaultCondition"): - default_condition = state.defaultCondition - if default_condition: - self.natural_transition( - self.current_state.name, default_condition.transition, "default" - ) - - def end_transition(self): - if hasattr(self.current_state, "end") and self.current_state.end: - self.end_state(self.current_state.name) - - def natural_transition( - self, - source: str, - target: Union[str, Transition], - label: Optional[str] = None, - ): - if target: - if isinstance(target, Transition): - desc_transition = target.nextState - else: - desc_transition = target - if source not in self.state_machine.states.keys(): - self.state_machine.add_states(source) - if desc_transition not in self.state_machine.states.keys(): - self.state_machine.add_states(desc_transition) - self.state_machine.add_transition( - trigger=label if label else "", source=source, dest=desc_transition - ) - - def error_transitions(self): - if hasattr(self.current_state, "onErrors") and ( - on_errors := self.current_state.onErrors - ): - for error in on_errors: - self.natural_transition( - self.current_state.name, - error.transition, - error.errorRef, - ) - - def compensated_by_transition(self): - compensated_by = self.current_state.compensatedBy - if compensated_by: - self.natural_transition( - self.current_state.name, compensated_by, "compensated by" - ) - - def definitions(self): - state_type = self.current_state.type - if state_type == "sleep": - self.sleep_state_details() - elif state_type == "event": - self.event_state_details() - elif state_type == "operation": - self.operation_state_details() - elif state_type == "parallel": - self.parallel_state_details() - elif state_type == "switch": - if self.current_state.dataConditions: - self.data_based_switch_state_details() - elif self.current_state.eventConditions: - self.event_based_switch_state_details() - else: - raise Exception( - f"Unexpected switch type;\n state value= {self.current_state}" - ) - elif state_type == "inject": - self.inject_state_details() - elif state_type == "foreach": - self.foreach_state_details() - elif state_type == "callback": - self.callback_state_details() - else: - raise Exception( - f"Unexpected type= {state_type};\n state value= {self.current_state}" - ) - - def parallel_state_details(self): - if isinstance(self.current_state, ParallelState): - self.state_to_machine_state(["parallel_state", "state"]) - - state_name = self.current_state.name - branches = self.current_state.branches - if branches: - if self.get_actions: - self.state_machine.get_state(state_name).initial = [] - for branch in branches: - if hasattr(branch, "actions") and branch.actions: - branch_name = branch.name - self.state_machine.get_state(state_name).add_substates( - branch_state := self.state_machine.state_cls( - branch_name - ) - ) - self.state_machine.get_state(state_name).initial.append( - branch_name - ) - branch_state.tags = ["branch"] - branch_state.metadata = { - "branch": self.current_state.serialize().__dict__ - } - self.generate_actions_info( - machine_state=branch_state, - state_name=f"{state_name}.{branch_name}", - actions=branch.actions, - ) - - def event_based_switch_state_details(self): - if isinstance(self.current_state, EventBasedSwitchState): - self.state_to_machine_state( - ["event_based_switch_state", "switch_state", "state"] - ) - - def data_based_switch_state_details(self): - if isinstance(self.current_state, DataBasedSwitchState): - self.state_to_machine_state( - ["data_based_switch_state", "switch_state", "state"] - ) - - def inject_state_details(self): - if isinstance(self.current_state, InjectState): - self.state_to_machine_state(["inject_state", "state"]) - - def operation_state_details(self): - if isinstance(self.current_state, OperationState): - machine_state = self.state_to_machine_state(["operation_state", "state"]) - self.generate_actions_info( - machine_state=machine_state, - state_name=self.current_state.name, - actions=self.current_state.actions, - action_mode=self.current_state.actionMode, - ) - - def sleep_state_details(self): - if isinstance(self.current_state, SleepState): - self.state_to_machine_state(["sleep_state", "state"]) - - def event_state_details(self): - if isinstance(self.current_state, EventState): - state = self.state_to_machine_state(["event_state", "state"]) - if self.get_actions: - if on_events := self.current_state.onEvents: - state.initial = [] if len(on_events) > 1 else on_events[0] - for i, oe in enumerate(on_events): - state.add_substate( - oe_state := self.state_machine.state_cls( - oe_name := f"onEvent {i}" - ) - ) - - # define initial state - if i == 0 and len(on_events) > 1: - state.initial = [oe_state.name] - elif i == 0 and len(on_events) == 1: - state.initial = oe_state.name - else: - state.initial.append(oe_state.name) - - event_names = [] - for ie, event in enumerate(oe.eventRefs): - oe_state.add_substate( - ns := self.state_machine.state_cls(event) - ) - ns.tags = ["event"] - self.get_action_event(state=ns, e_name=event) - event_names.append(event) - - # define initial state - if ie == 0 and len(oe.eventRefs) > 1: - oe_state.initial = [event] - elif ie == 0 and len(oe.eventRefs) == 1: - oe_state.initial = event - else: - oe_state.initial.append(event) - - if self.current_state.exclusive and oe.actions: - oe_state.add_substate( - ns := self.state_machine.state_cls( - action_name := f"action {ie}" - ) - ) - self.state_machine.add_transition( - trigger="", - source=f"{self.current_state.name}.{oe_name}.{event}", - dest=f"{self.current_state.name}.{oe_name}.{action_name}", - ) - self.generate_actions_info( - machine_state=ns, - state_name=f"{self.current_state.name}.{oe_name}.{action_name}", - actions=oe.actions, - action_mode=oe.actionMode, - ) - if not self.current_state.exclusive and oe.actions: - self.generate_actions_info( - machine_state=oe_state, - state_name=f"{self.current_state.name}.{oe_name}", - actions=oe.actions, - action_mode=oe.actionMode, - initial_states=event_names, - ) - - def foreach_state_details(self): - if isinstance(self.current_state, ForEachState): - self.state_to_machine_state(["foreach_state", "state"]) - self.state_machine.add_transition( - trigger=f"{self.current_state.iterationParam} IN {self.current_state.inputCollection}", - source=self.current_state.name, - dest=self.current_state.name, - ) - self.generate_actions_info( - machine_state=self.state_machine.get_state(self.current_state.name), - state_name=self.current_state.name, - actions=self.current_state.actions, - action_mode=self.current_state.mode, - ) - - def callback_state_details(self): - if isinstance(self.current_state, CallbackState): - self.state_to_machine_state(["callback_state", "state"]) - action = self.current_state.action - if action: - self.generate_actions_info( - machine_state=self.state_machine.get_state(self.current_state.name), - state_name=self.current_state.name, - actions=[action], - ) - - def state_to_machine_state(self, tags: List[str]) -> NestedState: - state_name = self.current_state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - (ns := self.state_machine.get_state(state_name)).tags = tags - ns.metadata = {"state": self.current_state.serialize().__dict__} - return ns - - def get_subflow_state( - self, machine_state: NestedState, state_name: str, actions: List[Action] - ): - added_states = {} - for i, action in enumerate(actions): - if action.subFlowRef: - if isinstance(action.subFlowRef, str): - workflow_id = action.subFlowRef - workflow_version = None - else: - workflow_id = action.subFlowRef.workflowId - workflow_version = action.subFlowRef.version - none_found = True - for sf in self.subflows: - if sf.id == workflow_id and ( - (workflow_version and sf.version == workflow_version) - or not workflow_version - ): - none_found = False - new_machine = CustomHierarchicalMachine( - model=None, initial=None, auto_transitions=False - ) - - # Generate the state machine for the subflow - StateMachineGenerator( - workflow=sf, - state_machine=new_machine, - get_actions=self.get_actions, - subflows=self.subflows, - ).generate() - - # Convert the new_machine into a NestedState - added_states[i] = self.subflow_state_name( - action=action, subflow=sf - ) - nested_state = self.state_machine.state_cls(added_states[i]) - nested_state.tags = ["subflow"] - machine_state.add_substate(nested_state) - self.state_machine_to_nested_state( - state_name=state_name, - state_machine=new_machine, - nested_state=nested_state, - ) - - if none_found: - warnings.warn( - f"Specified subflow [{workflow_id} {workflow_version if workflow_version else ''}] not found.", - category=UserWarning, - ) - return added_states - - def generate_actions_info( - self, - machine_state: NestedState, - state_name: str, - actions: List[Dict[str, Action]], - action_mode: str = "sequential", - initial_states: List[str] = [], - ): - if self.get_actions: - parallel_states = [] - if actions: - new_subflows_names = self.get_subflow_state( - machine_state=machine_state, state_name=state_name, actions=actions - ) - for i, action in enumerate(actions): - name = None - if action.functionRef: - name = ( - self.get_function_name(action.functionRef) - if isinstance(action.functionRef, str) - else ( - action.functionRef.refName - if isinstance(action.functionRef, FunctionRef) - else None - ) - ) - if name not in machine_state.states.keys(): - machine_state.add_substate( - ns := self.state_machine.state_cls(name) - ) - ns.tags = ["function"] - self.get_action_function(state=ns, f_name=name) - elif action.subFlowRef: - name = new_subflows_names.get(i) - elif action.eventRef: - name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" - if name not in machine_state.states.keys(): - machine_state.add_substate( - ns := self.state_machine.state_cls(name) - ) - ns.tags = ["event"] - self.get_action_event( - state=ns, - e_name=action.eventRef.triggerEventRef, - er_name=action.eventRef.resultEventRef, - ) - if name: - if action_mode == "sequential": - if i < len(actions) - 1: - # get next name - next_name = None - if actions[i + 1].functionRef: - next_name = ( - self.get_function_name( - actions[i + 1].functionRef - ) - if isinstance(actions[i + 1].functionRef, str) - else ( - actions[i + 1].functionRef.refName - if isinstance( - actions[i + 1].functionRef, FunctionRef - ) - else None - ) - ) - if ( - next_name - not in self.state_machine.get_state( - state_name - ).states.keys() - ): - machine_state.add_substate( - ns := self.state_machine.state_cls( - next_name - ) - ) - ns.tags = ["function"] - self.get_action_function( - state=ns, f_name=next_name - ) - elif actions[i + 1].subFlowRef: - next_name = new_subflows_names.get(i + 1) - elif actions[i + 1].eventRef: - next_name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" - if ( - next_name - not in self.state_machine.get_state( - state_name - ).states.keys() - ): - machine_state.add_substate( - ns := self.state_machine.state_cls( - next_name - ) - ) - ns.tags = ["event"] - self.get_action_event( - state=ns, - e_name=action.eventRef.triggerEventRef, - er_name=action.eventRef.resultEventRef, - ) - self.state_machine.add_transition( - trigger="", - source=f"{state_name}.{name}", - dest=f"{state_name}.{next_name}", - ) - if i == 0 and not initial_states: - machine_state.initial = name - elif i == 0 and initial_states: - for init_s in initial_states: - self.state_machine.add_transition( - trigger="", - source=f"{state_name}.{init_s}", - dest=f"{state_name}.{name}", - ) - elif action_mode == "parallel": - parallel_states.append(name) - if action_mode == "parallel" and not initial_states: - machine_state.initial = parallel_states - elif action_mode == "parallel" and initial_states: - for init_s in initial_states: - for ps in parallel_states: - self.state_machine.add_transition( - trigger="", - source=f"{state_name}.{init_s}", - dest=f"{state_name}.{ps}", - ) - - def get_action_function(self, state: NestedState, f_name: str): - if self.workflow.functions: - for function in self.workflow.functions: - current_function = function.serialize().__dict__ - if current_function["name"] == f_name: - state.metadata = {"function": current_function} - break - - def get_action_event(self, state: NestedState, e_name: str, er_name: str = None): - if self.workflow.events: - state.metadata = {"event": (None if er_name is None else {})} - for event in self.workflow.events: - current_event = event.serialize().__dict__ - if current_event["name"] == e_name: - if type(state.metadata["event"]) == dict: - state.metadata["event"]["trigger"] = current_event - else: - state.metadata["event"] = current_event - if er_name and current_event["name"] == er_name: - state.metadata["event"]["result"] = current_event - - def subflow_state_name(self, action: Action, subflow: Workflow): - return ( - action.name - if action.name - else f"{subflow.id}/{subflow.version.replace(NestedState.separator, '-')}" - ) - - def add_all_sub_states( - self, - original_state: Union[NestedState, CustomHierarchicalMachine], - new_state: NestedState, - ): - if len(original_state.states) == 0: - return - for substate in original_state.states.values(): - new_state.add_substate(ns := self.state_machine.state_cls(substate.name)) - ns.tags = substate.tags - ns.metadata = substate.metadata - self.add_all_sub_states(substate, ns) - new_state.initial = original_state.initial - - def state_machine_to_nested_state( - self, - state_name: str, - state_machine: CustomHierarchicalMachine, - nested_state: NestedState, - ) -> NestedState: - self.add_all_sub_states(state_machine, nested_state) - - for trigger, event in state_machine.events.items(): - for transition_l in event.transitions.values(): - for transition in transition_l: - source = transition.source - dest = transition.dest - self.state_machine.add_transition( - trigger=trigger, - source=f"{state_name}.{nested_state.name}.{source}", - dest=f"{state_name}.{nested_state.name}.{dest}", - ) - - def get_function_name( - self, fn_ref: Union[Dict[str, Any], str, None] - ) -> Optional[str]: - if isinstance(fn_ref, dict) and "refName" in fn_ref: - return fn_ref["refName"] - elif isinstance(fn_ref, str): - return fn_ref - return None - - def end_state(self, name, condition=None): - if name not in self.state_machine.states.keys(): - self.state_machine.add_states(name) - - if not condition: - self.state_machine.get_state(name).final = True - else: - if "[*]" not in self.state_machine.states.keys(): - self.state_machine.add_states("[*]") - self.state_machine.get_state("[*]").final = True - self.state_machine.add_transition( - trigger=condition if condition else "", source=name, dest="[*]" - ) diff --git a/serverlessworkflow/sdk/state_machine_helper.py b/serverlessworkflow/sdk/state_machine_helper.py deleted file mode 100644 index 9436d93..0000000 --- a/serverlessworkflow/sdk/state_machine_helper.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import List -from serverlessworkflow.sdk.workflow import Workflow -from serverlessworkflow.sdk.state_machine_generator import StateMachineGenerator -from serverlessworkflow.sdk.state_machine_extensions import ( - CustomGraphMachine, - CustomHierarchicalGraphMachine, -) - - -class StateMachineHelper: - FINAL_NODE_STYLE = {"peripheries": "2", "color": "red"} - INITIAL_NODE_STYLE = {"peripheries": "2", "color": "green"} - TAGS = [ - "parallel_state", - "switch_state", - "inject_state", - "operation_state", - "sleep_state", - "event_state", - "foreach_state", - "callback_state", - "subflow", - "function", - "event", - "branch", - ] - COLORS = [ - "#8dd3c7", - "#ffffb3", - "#bebada", - "#fb8072", - "#80b1d3", - "#fdb462", - "#b3de69", - "#fccde5", - "#d9d9d9", - "#bc80bd", - "#ccebc5", - "#ffed6f", - ] - - def __init__( - self, - workflow: Workflow, - subflows: List[Workflow] = [], - get_actions=False, - title="", - ): - self.subflows = subflows - self.get_actions = get_actions - - machine_type = ( - CustomHierarchicalGraphMachine if self.get_actions else CustomGraphMachine - ) - - # Generate machine - self.machine = machine_type( - model=None, - initial=None, - show_conditions=True, - auto_transitions=False, - title=title, - ) - StateMachineGenerator( - workflow=workflow, - state_machine=self.machine, - get_actions=self.get_actions, - subflows=subflows, - ).generate() - - delattr(self.machine, "get_graph") - del self.machine.style_attributes["node"]["active"] - del self.machine.style_attributes["graph"]["active"] - self.machine.add_model(machine_type.self_literal) - - def draw(self, filename: str, graph_engine="pygraphviz"): - if graph_engine == "mermaid": - self.machine.graph_cls = self.machine._init_graphviz_engine( - graph_engine="mermaid" - ) - self.machine.model_graphs[id(self.machine.model)] = self.machine.graph_cls( - self.machine - ) - - # Define style - for name in ( - self.machine.get_nested_state_names() - if self.get_actions - else self.machine.states.keys() - ): - if self.machine.get_state(name).final or self.machine.initial == name: - self.machine.style_attributes["node"][name] = ( - self.FINAL_NODE_STYLE - if self.machine.get_state(name).final - else self.INITIAL_NODE_STYLE - ) - self.machine.model_graphs[id(self.machine.model)].set_node_style( - name, name - ) - - for tag in self.machine.get_state(name).tags: - if tag in self.TAGS: - self.machine.style_attributes["node"][name] = { - "fillcolor": self.COLORS[self.TAGS.index(tag)] - } - self.machine.model_graphs[id(self.machine.model)].set_node_style( - name, name - ) - break - - self.machine.get_graph().draw(filename, prog="dot") diff --git a/serverlessworkflow/sdk/sub_flow_ref.py b/serverlessworkflow/sdk/sub_flow_ref.py deleted file mode 100644 index 94aaec9..0000000 --- a/serverlessworkflow/sdk/sub_flow_ref.py +++ /dev/null @@ -1,16 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class SubFlowRef(SwfBase): - workflowId: str = None - version: str = None - onParentComplete: str = None - invoke: str = None - - def __init__(self, - workflowId: str = None, - version: str = None, - onParentComplete: str = None, - invoke: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/swf_base.py b/serverlessworkflow/sdk/swf_base.py deleted file mode 100644 index 80ebc8d..0000000 --- a/serverlessworkflow/sdk/swf_base.py +++ /dev/null @@ -1,175 +0,0 @@ -from __future__ import annotations - -import copy -import dataclasses -import traceback -from abc import ABC, abstractmethod -from typing import Any - - -class HydratableType(ABC): - @abstractmethod - def hydrate(self, value): - pass - - -class ArrayTypeOf(HydratableType): - def __init__(self, Type): - self.Type = Type - - def hydrate(self, value): - return [self.Type(**v) if type(v) is not self.Type else v for v in value] - - -class ComplexTypeOf(HydratableType): - def __init__(self, Type): - self.Type = Type - - def hydrate(self, value): - return self.Type(**value) if type(value) is not self.Type else value - - -class SimpleTypeOf(HydratableType): - def __init__(self, Type): - self.Type = Type - - def hydrate(self, value): - if type(value) is self.Type: - return value - - -class UnionTypeOf(HydratableType): - types: [HydratableType] - - def __init__(self, types: [HydratableType]): - self.types = types - - def hydrate(self, value): - - for t in self.types: - if t.hydrate(value) is not None: - return t.hydrate(value) - - return None - - -class HydratableParameter: - def __init__(self, value: Any): - self.complex_type = None - self.simple_type = None - self.value = value - - def hydrateAs(self, hydratable: HydratableType): - return hydratable.hydrate(self.value) - - -@dataclasses.dataclass -class Field: - def __init__(self, key: str, value: Any): - self.key = key - self.value = value - - -class SwfBase: - def __init__(self, fields, kwargs, f_hydration, _default_values={}): - self._default_values = {} - self._initial_values = {} - - _attributes: [Field] = [] - _initial_values = {} - k: str - for k in list(fields): - if k in ["self", "kwargs"]: - continue - if k.startswith("_"): - continue - - final_value = fields.get(k) - - if final_value == "true": - final_value = True - - if final_value == "false": - final_value = False - - _initial_values[k] = final_value - - if final_value is None and _default_values.get(k) is not None: - final_value = _default_values.get(k) - - if final_value is None: - continue - - final_value = f_hydration(k, final_value) - - if final_value is not None: - key_ = k.replace("_", "") - _attributes.append(Field(key_, final_value)) - - _attributes.append(Field("_initial_values", _initial_values)) - _attributes.append(Field("_default_values", _default_values)) - - for k in kwargs.keys(): - final_value = kwargs[k] - - if final_value == "true": - final_value = True - - if final_value == "false": - final_value = False - - _initial_values[k] = final_value - - if final_value is None and _default_values.get(k) is not None: - final_value = _default_values.get(k) - - if final_value is None: - continue - - final_value = f_hydration(k, final_value) - - if final_value is not None: - key_ = k.replace("_", "") - _attributes.append(Field(key_, final_value)) - - _attributes.append(Field("_initial_values", _initial_values)) - _attributes.append(Field("_default_values", _default_values)) - - for f in _attributes: - self.__setattr__(f.key, f.value) - - def serialize(self): - - try: - self_copy: SwfBase = copy.deepcopy(self) - - for k in self_copy.__dict__: - attribute_value = self_copy.__getattribute__(k) - if isinstance(attribute_value, list): - elements: list = attribute_value - for element in elements.copy(): - if isinstance(element, SwfBase): - elements.remove(element) - serialize = element.serialize() - elements.append(serialize) - - if isinstance(attribute_value, SwfBase): - self_copy.__setattr__(k, attribute_value.serialize()) - - for k in self_copy._default_values.keys(): - if self_copy._initial_values.get(k) is None: - delattr(self_copy, k) - - for k in self_copy.__dict__.copy(): - if k.startswith("_"): - delattr(self_copy, k) - - return self_copy - - except Exception as e: - traceback.print_exc() - raise e - - @staticmethod - def default_hydration(property_key, property_value): - return property_value diff --git a/serverlessworkflow/sdk/tasks.py b/serverlessworkflow/sdk/tasks.py new file mode 100644 index 0000000..a36703b --- /dev/null +++ b/serverlessworkflow/sdk/tasks.py @@ -0,0 +1,378 @@ +"""Task classes for Serverless Workflow SDK v1.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from serverlessworkflow.sdk.base import TaskItem + +from serverlessworkflow.sdk.base import ( + Duration, + Error, + ErrorFilter, + ExternalResource, + FlowDirective, + TaskBase, +) +from serverlessworkflow.sdk.call_tasks import SubscriptionIterator +from serverlessworkflow.sdk.events import EventConsumptionStrategy, EventProperties +from serverlessworkflow.sdk.retry import RetryPolicy + +# Do Task + + +@dataclass +class DoTask(TaskBase): + """Execute a list of tasks in sequence.""" + + do: list[TaskItem] | None = None + + def __post_init__(self): + """Validate do task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.do is None: + raise ValueError("do must be specified for DoTask") + + +# Fork Task + + +@dataclass +class ForkConfiguration: + """Configuration for fork task.""" + + branches: list[TaskItem] + compete: bool = False + + +@dataclass +class ForkTask(TaskBase): + """Execute multiple tasks concurrently.""" + + fork: ForkConfiguration | None = None + + def __post_init__(self): + """Validate fork task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.fork is None: + raise ValueError("fork must be specified for ForkTask") + + +# Emit Task + + +@dataclass +class EmitEventConfiguration: + """Configuration for event emission.""" + + with_: EventProperties = field(metadata={"alias": "with"}) + + +@dataclass +class EmitConfiguration: + """Configuration for emit task.""" + + event: dict[str, Any] # Event definition with 'with' property + + +@dataclass +class EmitTask(TaskBase): + """Emit an event.""" + + emit: EmitConfiguration | None = None + + def __post_init__(self): + """Validate emit task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.emit is None: + raise ValueError("emit must be specified for EmitTask") + + +# For Task + + +@dataclass +class ForConfiguration: + """Configuration for for loop.""" + + in_: str = field(metadata={"alias": "in"}) # Runtime expression + each: str | None = None + at: str | None = None + + +@dataclass +class ForTask(TaskBase): + """Iterate over a collection.""" + + for_: ForConfiguration | None = field(default=None, metadata={"alias": "for"}) + do: list[TaskItem] | None = None + while_: str | None = field(default=None, metadata={"alias": "while"}) # Runtime expression + + def __post_init__(self): + """Validate for task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.for_ is None: + raise ValueError("for_ must be specified for ForTask") + if self.do is None: + raise ValueError("do must be specified for ForTask") + + +# Listen Task + + +@dataclass +class ListenConfiguration: + """Configuration for listen task.""" + + to: EventConsumptionStrategy + read: str | None = None # data, envelope, or raw + + +@dataclass +class ListenTask(TaskBase): + """Listen for external events.""" + + listen: ListenConfiguration | None = None + foreach: SubscriptionIterator | None = None + + def __post_init__(self): + """Validate listen task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.listen is None: + raise ValueError("listen must be specified for ListenTask") + + +# Raise Task + + +@dataclass +class RaiseConfiguration: + """Configuration for raise task.""" + + error: Error | str # Error definition or reference + + +@dataclass +class RaiseTask(TaskBase): + """Raise an error.""" + + raise_: RaiseConfiguration | None = field(default=None, metadata={"alias": "raise"}) + + def __post_init__(self): + """Validate raise task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.raise_ is None: + raise ValueError("raise_ must be specified for RaiseTask") + + +# Run Task + + +@dataclass +class ContainerLifetime: + """Container lifetime configuration.""" + + cleanup: str = "never" # always, never, or eventually + after: Duration | None = None + + def __post_init__(self): + """Validate container lifetime configuration.""" + if self.cleanup == "eventually" and self.after is None: + raise ValueError("after must be specified when cleanup is 'eventually'") + if self.cleanup != "eventually" and self.after is not None: + raise ValueError("after can only be specified when cleanup is 'eventually'") + + +@dataclass +class ContainerConfiguration: + """Container configuration for run task.""" + + image: str + name: str | None = None + command: str | None = None + ports: dict[str, Any] | None = None + volumes: dict[str, Any] | None = None + environment: dict[str, str] | None = None + stdin: str | None = None + arguments: list[str] | None = None + lifetime: ContainerLifetime | None = None + + +@dataclass +class ScriptConfiguration: + """Script configuration for run task.""" + + language: str + code: str | None = None + source: ExternalResource | None = None + stdin: str | None = None + arguments: list[str] | None = None + environment: dict[str, str] | None = None + + def __post_init__(self): + """Validate script configuration.""" + if not self.code and not self.source: + raise ValueError("Must specify either code or source") + if self.code and self.source: + raise ValueError("Cannot specify both code and source") + + +@dataclass +class ShellConfiguration: + """Shell command configuration for run task.""" + + command: str + stdin: str | None = None + arguments: list[str] | None = None + environment: dict[str, str] | None = None + + +@dataclass +class WorkflowConfiguration: + """Subworkflow configuration for run task.""" + + namespace: str + name: str + version: str = "latest" + input: dict[str, Any] | None = None + + +@dataclass +class RunConfiguration: + """Configuration for run task.""" + + await_: bool | None = field( + default=None, metadata={"alias": "await"} + ) # defaults to True if not specified + return_: str | None = field( + default=None, metadata={"alias": "return"} + ) # stdout, stderr, code, all, or none (defaults to stdout if not specified) + container: ContainerConfiguration | None = None + script: ScriptConfiguration | None = None + shell: ShellConfiguration | None = None + workflow: WorkflowConfiguration | None = None + + def __post_init__(self): + """Validate run configuration.""" + processes = [self.container, self.script, self.shell, self.workflow] + set_processes = [p for p in processes if p is not None] + if len(set_processes) != 1: + raise ValueError("Must specify exactly one of: container, script, shell, or workflow") + + +@dataclass +class RunTask(TaskBase): + """Execute external processes.""" + + run: RunConfiguration | None = None + + def __post_init__(self): + """Validate run task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.run is None: + raise ValueError("run must be specified for RunTask") + + +# Set Task + + +@dataclass +class SetTask(TaskBase): + """Set data.""" + + set: dict[str, Any] | str | None = None # Data to set or runtime expression + + def __post_init__(self): + """Validate set task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.set is None: + raise ValueError("set must be specified for SetTask") + + +# Switch Task + + +@dataclass +class SwitchCase: + """A case within a switch task.""" + + when: str | None = None # Runtime expression + then: FlowDirective = "continue" + + +@dataclass +class SwitchTask(TaskBase): + """Conditional branching.""" + + switch: list[dict[str, SwitchCase]] | None = None # List of named cases + + def __post_init__(self): + """Validate switch task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.switch is None: + raise ValueError("switch must be specified for SwitchTask") + + +# Try Task + + +@dataclass +class CatchConfiguration: + """Configuration for error catching.""" + + errors: dict[str, ErrorFilter] | None = None + as_: str | None = field(default=None, metadata={"alias": "as"}) + when: str | None = None # Runtime expression + exceptWhen: str | None = None # Runtime expression + retry: RetryPolicy | str | None = None + do: list[TaskItem] | None = None + + +@dataclass +class TryTask(TaskBase): + """Handle errors gracefully.""" + + try_: list[TaskItem] | None = field(default=None, metadata={"alias": "try"}) + catch: CatchConfiguration | None = None + + def __post_init__(self): + """Validate try task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.try_ is None: + raise ValueError("try_ must be specified for TryTask") + if self.catch is None: + raise ValueError("catch must be specified for TryTask") + + +# Wait Task + + +@dataclass +class WaitTask(TaskBase): + """Pause execution for a duration.""" + + wait: Duration | None = None + + def __post_init__(self): + """Validate wait task configuration.""" + super().__post_init__() if hasattr(super(), "__post_init__") else None + if self.wait is None: + raise ValueError("wait must be specified for WaitTask") + + +# Union type for all tasks +Task = ( + DoTask + | ForkTask + | EmitTask + | ForTask + | ListenTask + | RaiseTask + | RunTask + | SetTask + | SwitchTask + | TryTask + | WaitTask +) diff --git a/serverlessworkflow/sdk/transition.py b/serverlessworkflow/sdk/transition.py deleted file mode 100644 index 347b307..0000000 --- a/serverlessworkflow/sdk/transition.py +++ /dev/null @@ -1,26 +0,0 @@ -import copy - -from serverlessworkflow.sdk.produce_event_def import ProduceEventDef -from serverlessworkflow.sdk.swf_base import ArrayTypeOf, HydratableParameter, SwfBase - - -class Transition(SwfBase): - nextState: str = None - produceEvents: [ProduceEventDef] = None - compensate: bool = None - - def __init__(self, - nextState: str = None, - produceEvents: [ProduceEventDef] = None, - compensate: bool = None, - **kwargs): - _default_values = {'compensate': False} - SwfBase.__init__(self, locals(), kwargs, Transition.f_hydration, - _default_values) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'produceEvents': - return HydratableParameter(value=p_value).hydrateAs(ArrayTypeOf(ProduceEventDef)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/transition_data_condition.py b/serverlessworkflow/sdk/transition_data_condition.py deleted file mode 100644 index fcc47b6..0000000 --- a/serverlessworkflow/sdk/transition_data_condition.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, SimpleTypeOf, ComplexTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class TransitionDataCondition(SwfBase): - name: str = None - condition: str = None - transition: (str | Transition) = None - metadata: Metadata = None - - def __init__(self, - name: str = None, - condition: str = None, - transition: (str | Transition) = None, - metadata: Metadata = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, TransitionDataCondition.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/transition_event_condition.py b/serverlessworkflow/sdk/transition_event_condition.py deleted file mode 100644 index 15f3fce..0000000 --- a/serverlessworkflow/sdk/transition_event_condition.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import copy - -from serverlessworkflow.sdk.event_data_filter import EventDataFilter -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, SimpleTypeOf, ComplexTypeOf, SwfBase -from serverlessworkflow.sdk.transition import Transition - - -class TransitionEventCondition(SwfBase): - name: str = None - eventRef: str = None - transition: (str | Transition) = None - eventDataFilter: EventDataFilter = None - metadata: Metadata = None - - def __init__(self, - name: str = None, - eventRef: str = None, - transition: (str | Transition) = None, - eventDataFilter: EventDataFilter = None, - metadata: Metadata = None, - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'transition': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(Transition)])) - - if p_key == 'eventDataFilter': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(EventDataFilter)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/workflow.py b/serverlessworkflow/sdk/workflow.py index 2d0e578..c6e0f98 100644 --- a/serverlessworkflow/sdk/workflow.py +++ b/serverlessworkflow/sdk/workflow.py @@ -1,183 +1,500 @@ +"""Main workflow classes for Serverless Workflow SDK v1.""" + from __future__ import annotations -import copy -import json - -import yaml - -from serverlessworkflow.sdk.auth_def import AuthDef -from serverlessworkflow.sdk.callback_state import CallbackState -from serverlessworkflow.sdk.databased_switch_state import DataBasedSwitchState -from serverlessworkflow.sdk.error_def import ErrorDef -from serverlessworkflow.sdk.event_based_switch_state import EventBasedSwitchState -from serverlessworkflow.sdk.event_def import EventDef -from serverlessworkflow.sdk.event_state import EventState -from serverlessworkflow.sdk.foreach_state import ForEachState -from serverlessworkflow.sdk.function import Function -from serverlessworkflow.sdk.inject_state import InjectState -from serverlessworkflow.sdk.metadata import Metadata -from serverlessworkflow.sdk.operation_state import OperationState -from serverlessworkflow.sdk.parallel_state import ParallelState -from serverlessworkflow.sdk.retry_def import RetryDef -from serverlessworkflow.sdk.sleep_state import SleepState -from serverlessworkflow.sdk.start_def import StartDef -from serverlessworkflow.sdk.state import State -from serverlessworkflow.sdk.swf_base import HydratableParameter, UnionTypeOf, SimpleTypeOf, ComplexTypeOf, \ - ArrayTypeOf, SwfBase -from serverlessworkflow.sdk.workflow_time_out import WorkflowTimeOut - - -class DataInputSchema: - schema: str - failOnValidationErrors: bool - - -class Workflow(SwfBase): - id: str = None - key: str = None - name: str = None - description: str = None - version: str = None - annotations: [str] = None - dataInputSchema: (str | DataInputSchema) = None - secrets: str = None # Secrets - constants: (str | dict[str, dict]) = None - start: (str | StartDef) = None - specVersion: str = None - expressionLang: str = None - timeouts: (str | WorkflowTimeOut) = None - errors: (str | [ErrorDef]) = None - keepActive: bool = None - metadata: Metadata = None - events: (str | [EventDef]) = None - functions: (str | [Function]) = None - autoRetries: bool = None - retries: (str | [RetryDef]) = None - auth: (str, [AuthDef]) = None - states: [State] = None - - def __init__(self, - id: str = None, - key: str = None, - name: str = None, - version: str = None, - description: str = None, - specVersion: str = None, - annotations: [str] = None, - dataInputSchema: (str | DataInputSchema) = None, - secrets: str = None, # Secrets - constants: (str | dict[str, dict]) = None, - start: (str | StartDef) = None, - expressionLang: str = None, - timeouts: (str | WorkflowTimeOut) = None, - errors: (str | [ErrorDef]) = None, - keepActive: bool = None, - metadata: Metadata = None, - events: (str | [EventDef]) = None, - autoRetries: bool = None, - retries: (str | [RetryDef]) = None, - auth: (str | [AuthDef]) = None, - states: [State] = None, - functions: (str | [Function]) = None - , **kwargs): - - _default_values = {'expressionLang': 'jq', 'keepActive': True} - SwfBase.__init__(self, locals(), kwargs, Workflow.f_hydration, - _default_values) - - def to_json(self) -> str: - - self_copy = self.serialize() - return json.dumps(self_copy, - default=lambda o: o.__dict__, - indent=4) - - def to_yaml(self): - - self_copy = self.serialize() - yaml.emitter.Emitter.process_tag = lambda x: None - return yaml.dump(self_copy, - sort_keys=False, - allow_unicode=True, - ) +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from serverlessworkflow.sdk.base import Error + from serverlessworkflow.sdk.tasks import Task + +from serverlessworkflow.sdk.authentication import ReferenceableAuthenticationPolicy +from serverlessworkflow.sdk.base import Duration, Input, Output, TaskItem, Timeout +from serverlessworkflow.sdk.endpoint import Catalog +from serverlessworkflow.sdk.events import EventConsumptionStrategy +from serverlessworkflow.sdk.retry import RetryPolicy + + +@dataclass +class Document: + """Documents the workflow.""" + + dsl: str + namespace: str + name: str + version: str + title: str | None = None + summary: str | None = None + tags: dict[str, Any] | None = None + metadata: dict[str, Any] | None = None + + def __post_init__(self): + """Validate document metadata.""" + # Validate patterns if needed + # dsl, namespace, name should match their respective patterns + pass + + +@dataclass +class Extension: + """Definition of a task extension.""" + + extend: str # call, composite, emit, for, listen, raise, run, set, switch, try, wait, all + when: str | None = None + before: list[TaskItem] | None = None + after: list[TaskItem] | None = None + + +@dataclass +class Use: + """Defines reusable workflow components.""" + + authentications: dict[str, ReferenceableAuthenticationPolicy] | None = None + errors: dict[str, Error] | None = None + extensions: list[dict[str, Extension]] | None = None + functions: dict[str, Task] | None = None + retries: dict[str, RetryPolicy] | None = None + secrets: list[str] | None = None + timeouts: dict[str, Timeout] | None = None + catalogs: dict[str, Catalog] | None = None + + +@dataclass +class Schedule: + """Schedules the workflow.""" + + every: Duration | None = None + cron: str | None = None + after: Duration | None = None + on: EventConsumptionStrategy | None = None + + def __post_init__(self): + """Validate schedule configuration.""" + # At least one scheduling option should be set + if not any([self.every, self.cron, self.after, self.on]): + raise ValueError("At least one schedule option must be specified") + + +class Workflow: + """The main workflow definition.""" + + document: Document + do: list[TaskItem] + input: Input | None = None + use: Use | None = None + timeout: Timeout | str | None = None + output: Output | None = None + schedule: Schedule | None = None + + def __init__( + self, + document: Document, + do: list[TaskItem], + input: Input | None = None, + use: Use | None = None, + timeout: Timeout | str | None = None, + output: Output | None = None, + schedule: Schedule | None = None, + ): + """Initialize a new Workflow instance. + + Args: + document: Workflow document metadata + do: List of tasks to execute + input: Input configuration + use: Resources to use + timeout: Workflow timeout + output: Output configuration + schedule: Workflow schedule + """ + self.document = document + self.do = do + self.input = input + self.use = use + self.timeout = timeout + self.output = output + self.schedule = schedule + + def serialize(self) -> dict[str, Any]: + """Serializes the Workflow to a dictionary.""" + result = { + "document": self._serialize_value(self.document), + "do": self._serialize_value(self.do), + } + if self.input is not None: + result["input"] = self._serialize_value(self.input) + if self.use is not None: + result["use"] = self._serialize_value(self.use) + if self.timeout is not None: + result["timeout"] = self._serialize_value(self.timeout) + if self.output is not None: + result["output"] = self._serialize_value(self.output) + if self.schedule is not None: + result["schedule"] = self._serialize_value(self.schedule) + return result @classmethod - def from_source(cls, source: str): + def _serialize_value(cls, value: Any) -> Any: + """Recursively serialize a value to JSON-compatible types. + + For dataclasses: + - Checks field metadata for explicit 'alias' to use as the output key + - Falls back to stripping trailing underscore from field names (Python keyword convention) + - Skips None values to match YAML's sparse representation + + This approach leverages dataclass field metadata where defined, and uses + convention (trailing underscore) for Python reserved keywords elsewhere. + """ + from dataclasses import fields, is_dataclass + + # Handle None + if value is None: + return None + + # Special handling for TaskItem: convert to single-key dict format + if isinstance(value, TaskItem): + # Return as {'taskName': taskObject} format + return {value.name: cls._serialize_value(value.task)} + + # Handle dataclasses + if is_dataclass(value) and not isinstance(value, type): + # Convert dataclass to dict and handle field aliases + result = {} + for f in fields(value): + field_value = getattr(value, f.name) + # Skip None values to match YAML serialization + if field_value is None: + continue + # Check if field has an alias in metadata + key = f.metadata["alias"] if f.metadata and "alias" in f.metadata else f.name + result[key] = cls._serialize_value(field_value) + return result + + # Handle dictionaries + if isinstance(value, dict): + return {k: cls._serialize_value(v) for k, v in value.items()} + + # Handle lists/tuples + if isinstance(value, (list | tuple)): + return [cls._serialize_value(item) for item in value] + + # Return as-is for other types + return value + + @classmethod + def from_yaml(cls, yaml_str: str) -> Workflow: + """Parses a Workflow from a YAML string.""" + import yaml + + # Create a custom loader that doesn't convert 'on', 'yes', 'no', etc. to booleans + class StringBoolLoader(yaml.SafeLoader): + pass + + # Remove the implicit boolean resolvers that convert on/off/yes/no to booleans + # This ensures 'on:' stays as the string 'on' instead of becoming True + for ch in ["o", "O", "y", "Y", "n", "N"]: + if ch in StringBoolLoader.yaml_implicit_resolvers: + StringBoolLoader.yaml_implicit_resolvers[ch] = [ + x + for x in StringBoolLoader.yaml_implicit_resolvers[ch] + if x[0] != "tag:yaml.org,2002:bool" + ] + try: - loaded_data = yaml.safe_load(source) - return cls(**loaded_data) - except Exception: - raise Exception("Format not supported") + loaded_data = yaml.load(yaml_str, Loader=StringBoolLoader) + return cls._deserialize(loaded_data) + except yaml.YAMLError as e: + raise ValueError("Invalid YAML content") from e - def __repr__(self): - return "{!r}".format(self.__dict__) - - @staticmethod - def f_hydration(p_key, p_value): - - if p_key == 'dataInputSchema': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(DataInputSchema)])) - - if p_key == 'constants': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(dict)])) - if p_key == 'start': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(StartDef)])) - - if p_key == 'timeouts': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ComplexTypeOf(WorkflowTimeOut)])) - - if p_key == 'errors': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ArrayTypeOf(ErrorDef)])) - - if p_key == 'events': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ArrayTypeOf(EventDef)])) - - if p_key == 'retries': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ArrayTypeOf(RetryDef)])) - - if p_key == 'auth': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ArrayTypeOf(AuthDef)])) - - if p_key == 'states': - return [Workflow.hydrate_state(v) if not isinstance(v, State) else v for v in p_value] - - if p_key == 'functions': - return HydratableParameter(value=p_value).hydrateAs(UnionTypeOf([SimpleTypeOf(str), - ArrayTypeOf(Function)])) - - return copy.deepcopy(p_value) - - @staticmethod - def hydrate_state(raw_state: State): - - state = State(**raw_state) - if state.is_inject_state(): - return InjectState(**raw_state) - elif state.is_operation_state(): - return OperationState(**raw_state) - elif state.is_foreach_state(): - return ForEachState(**raw_state) - elif state.is_sleep_state(): - return SleepState(**raw_state) - elif state.is_switch_state(): - if hasattr(state, "dataConditions"): - return DataBasedSwitchState(**raw_state) - if hasattr(state, "eventConditions"): - return EventBasedSwitchState(**raw_state) - raise Exception(f"Unexpected switch type in {raw_state}") - elif state.is_callback_state(): - return CallbackState(**raw_state) - elif state.is_parallel_state(): - return ParallelState(**raw_state) - elif state.is_event_state(): - return EventState(**raw_state) + @classmethod + def _deserialize(cls, data: dict[str, Any]) -> Workflow: + """Deserialize a dictionary into a Workflow with proper type conversion.""" + # Convert document dict to Document dataclass + document_data = data.get("document") + if not isinstance(document_data, dict): + raise ValueError("document field is required and must be a dict") + document = Document(**document_data) if isinstance(document_data, dict) else document_data + + # Convert do list items + do_list = data.get("do", []) + converted_do: list[TaskItem] = [] + for item in do_list: + if isinstance(item, dict): + # Each item is a dict with one key (task name) and value (task config) + for task_name, task_config in item.items(): + if isinstance(task_config, dict): + # Determine task type and convert + converted_task = cls._convert_task(task_config) + # Create TaskItem from the name and task + task_item = TaskItem(name=task_name, task=converted_task) + converted_do.append(task_item) + else: + # If task_config is already a task object, wrap it + task_item = TaskItem(name=task_name, task=task_config) + converted_do.append(task_item) + else: + # Assume it's already a TaskItem + if isinstance(item, TaskItem): + converted_do.append(item) + + # Convert other optional fields + input_data = data.get("input") + use_data = data.get("use") + timeout_data = data.get("timeout") + output_data = data.get("output") + schedule_data = data.get("schedule") + + # Convert Schedule if present + schedule: Schedule | None = None + if schedule_data is not None: + if isinstance(schedule_data, dict): + schedule = Schedule(**schedule_data) + elif isinstance(schedule_data, Schedule): + schedule = schedule_data + + return cls( + document=document, + do=converted_do, + input=input_data, + use=use_data, + timeout=timeout_data, + output=output_data, + schedule=schedule, + ) + + @classmethod + def _map_yaml_keys_to_python(cls, data: dict[str, Any]) -> dict[str, Any]: + """Map YAML keys to Python-safe attribute names (handle reserved keywords). + + Python reserved keywords in YAML are mapped to their underscore-suffixed equivalents. + For example: 'for' -> 'for_', 'with' -> 'with_', 'if' -> 'if_', etc. + + This mapping is based on Python's convention for avoiding reserved keywords. + """ + # Python keywords that need underscore suffix + # See: https://docs.python.org/3/reference/lexical_analysis.html#keywords + python_keywords = { + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "try", + "while", + "with", + "yield", + } + + result = {} + for key, value in data.items(): + # If key is a reserved word, append underscore + python_key = f"{key}_" if key in python_keywords else key + result[python_key] = value + return result + + @classmethod + def _convert_task(cls, task_config: dict[str, Any]) -> Any: + """Convert a task configuration dict to the appropriate task type.""" + from .call_tasks import ( + CallA2ATask, + CallAsyncApiTask, + CallFunctionTask, + CallGrpcTask, + CallHttpTask, + CallMcpTask, + CallOpenApiTask, + ) + from .tasks import ( + DoTask, + EmitTask, + ForkTask, + ForTask, + ListenTask, + RaiseTask, + RunTask, + SetTask, + SwitchTask, + TryTask, + WaitTask, + ) + + # Map YAML keys to Python attribute names + mapped_config = cls._map_yaml_keys_to_python(task_config) + + # Determine task type based on which key is present + if "call" in mapped_config: + # Handle call tasks based on the call type + from .call_tasks import ( + CallA2AArguments, + CallAsyncApiArguments, + CallGrpcArguments, + CallHttpArguments, + CallMcpArguments, + CallOpenApiArguments, + ) + + call_type = mapped_config["call"] + # Remove 'call' from config as it's set via field(init=False) for most call tasks + config_without_call = {k: v for k, v in mapped_config.items() if k != "call"} + + # Convert with_ argument to appropriate type + if "with_" in config_without_call and isinstance(config_without_call["with_"], dict): + with_data = config_without_call["with_"] + + # Convert endpoint string to Endpoint if needed + if "endpoint" in with_data and isinstance(with_data["endpoint"], str): + with_data = {**with_data, "endpoint": with_data["endpoint"]} + + if call_type == "http": + config_without_call["with_"] = CallHttpArguments(**with_data) + elif call_type == "openapi": + # Convert document.endpoint if it's a string + if ( + "document" in with_data + and isinstance(with_data["document"], dict) + and "endpoint" in with_data["document"] + and isinstance(with_data["document"]["endpoint"], str) + ): + from .base import ExternalResource + + with_data["document"] = ExternalResource(**with_data["document"]) + config_without_call["with_"] = CallOpenApiArguments(**with_data) + elif call_type == "asyncapi": + # Similar conversion for AsyncAPI + if "document" in with_data and isinstance(with_data["document"], dict): + from .base import ExternalResource + + with_data["document"] = ExternalResource(**with_data["document"]) + config_without_call["with_"] = CallAsyncApiArguments(**with_data) + elif call_type == "grpc": + config_without_call["with_"] = CallGrpcArguments(**with_data) + elif call_type == "mcp": + config_without_call["with_"] = CallMcpArguments(**with_data) + elif call_type == "a2a": + config_without_call["with_"] = CallA2AArguments(**with_data) + + if call_type == "http": + return CallHttpTask(**config_without_call) + elif call_type == "openapi": + return CallOpenApiTask(**config_without_call) + elif call_type == "asyncapi": + return CallAsyncApiTask(**config_without_call) + elif call_type == "grpc": + return CallGrpcTask(**config_without_call) + elif call_type == "mcp": + return CallMcpTask(**config_without_call) + elif call_type == "a2a": + return CallA2ATask(**config_without_call) + else: + # Custom function call - needs 'call' parameter + return CallFunctionTask(**mapped_config) + elif "wait" in mapped_config: + # Convert wait field if it's a dict (Duration) + wait_value = mapped_config["wait"] + if isinstance(wait_value, dict): + wait_value = Duration(**wait_value) + return WaitTask(**{**mapped_config, "wait": wait_value}) + elif "do" in mapped_config and "for_" not in mapped_config and "try_" not in mapped_config: + return DoTask(**mapped_config) + elif "fork" in mapped_config: + return ForkTask(**mapped_config) + elif "emit" in mapped_config: + return EmitTask(**mapped_config) + elif "for_" in mapped_config: + return ForTask(**mapped_config) + elif "listen" in mapped_config: + return ListenTask(**mapped_config) + elif "raise_" in mapped_config: + return RaiseTask(**mapped_config) + elif "run" in mapped_config: + return RunTask(**mapped_config) + elif "set" in mapped_config: + return SetTask(**mapped_config) + elif "switch" in mapped_config: + return SwitchTask(**mapped_config) + elif "try_" in mapped_config: + return TryTask(**mapped_config) else: - raise Exception(f"Unexpected type in {raw_state}") + # Return as-is if we can't determine the type + return task_config + + @classmethod + def _convert_nested_objects(cls, data: dict[str, Any]) -> dict[str, Any | Duration]: + """Recursively convert nested dicts to appropriate types.""" + result: dict[str, Any | Duration] = {} + for key, value in data.items(): + if isinstance(value, dict): + # Try to convert to Duration if it looks like one + if any( + k in value + for k in [ + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "iso8601", + "expression", + ] + ): + result[key] = Duration(**value) + else: + result[key] = cls._convert_nested_objects(value) + else: + result[key] = value + return result + + def to_yaml(self) -> str: + """Serializes the Workflow to a YAML string.""" + import yaml + + serialized_data = self.serialize() + try: + return yaml.safe_dump(serialized_data, sort_keys=False) + except yaml.YAMLError as e: + raise ValueError("Error serializing to YAML") from e + + def render_graph(self, filename: str | None = None, engine: str = "graphviz") -> str: + """Render the workflow as a graph to a file. + + :param filename: Output filename (optional). + :param engine: Graph engine to use ("graphviz"). + :return: The graph as a DOT string. + """ + from .draw import render_workflow_graph + + return render_workflow_graph(self, filename, engine) + + def __eq__(self, other: Any) -> bool: + """Check equality with another workflow.""" + if not isinstance(other, Workflow): + return False + return self.serialize() == other.serialize() + + def __repr__(self): + """Return string representation of workflow.""" + return f"{self.__dict__!r}" diff --git a/serverlessworkflow/sdk/workflow_exec_timeout.py b/serverlessworkflow/sdk/workflow_exec_timeout.py deleted file mode 100644 index e5f7319..0000000 --- a/serverlessworkflow/sdk/workflow_exec_timeout.py +++ /dev/null @@ -1,14 +0,0 @@ -from serverlessworkflow.sdk.swf_base import SwfBase - - -class WorkflowExecTimeOut(SwfBase): - duration: str = None - interrupt: bool = None - runBefore: str = None - - def __init__(self, - duration: str = None, - interrupt: bool = None, - runBefore: str = None, - **kwargs): - SwfBase.__init__(self, locals(), kwargs, SwfBase.default_hydration) diff --git a/serverlessworkflow/sdk/workflow_time_out.py b/serverlessworkflow/sdk/workflow_time_out.py deleted file mode 100644 index a42a753..0000000 --- a/serverlessworkflow/sdk/workflow_time_out.py +++ /dev/null @@ -1,33 +0,0 @@ -import copy - -from serverlessworkflow.sdk.state_exec_timeout import StateExecTimeOut -from serverlessworkflow.sdk.swf_base import HydratableParameter, ComplexTypeOf, SwfBase -from serverlessworkflow.sdk.workflow_exec_timeout import WorkflowExecTimeOut - - -class WorkflowTimeOut(SwfBase): - workflowExecTimeOut: WorkflowExecTimeOut = None - stateExecTimeOut: StateExecTimeOut = None - actionExecTimeOut: str = None # ActionExecTimeOut - branchExecTimeOut: str = None # BranchExecTimeOut - eventTimeOut: str = None # EventTimeOut - - def __init__(self, - workflowExecTimeOut: WorkflowExecTimeOut = None, - stateExecTimeOut: StateExecTimeOut = None, - actionExecTimeOut: str = None, # ActionExecTimeOut - branchExecTimeOut: str = None, # BranchExecTimeOut - eventTimeOut: str = None, # EventTimeOut - **kwargs): - - SwfBase.__init__(self, locals(), kwargs, WorkflowTimeOut.f_hydration) - - @staticmethod - def f_hydration(p_key, p_value): - if p_key == 'workflowExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(WorkflowExecTimeOut)) - - if p_key == 'stateExecTimeOut': - return HydratableParameter(value=p_value).hydrateAs(ComplexTypeOf(StateExecTimeOut)) - - return copy.deepcopy(p_value) diff --git a/serverlessworkflow/sdk/workflow_validator.py b/serverlessworkflow/sdk/workflow_validator.py deleted file mode 100644 index be80b1a..0000000 --- a/serverlessworkflow/sdk/workflow_validator.py +++ /dev/null @@ -1,25 +0,0 @@ -import json - -import requests -from jsonschema.validators import validate - -from serverlessworkflow.sdk.workflow import Workflow - - -class WorkflowValidator: - json_schema_content: object = None - SCHEMAS_WORKFLOW_JSON = "https://serverlessworkflow.io/schemas/0.8/workflow.json" - - def __init__(self, workflow: Workflow): - self.workflow = workflow - - if not WorkflowValidator.json_schema_content: - file_json_schema = requests.get(self.SCHEMAS_WORKFLOW_JSON) - WorkflowValidator.json_schema_content = file_json_schema.json() - - def validate(self): - workflow = json.loads(self.workflow.to_json()) - validate(workflow, self.json_schema_content) - - def __repr__(self): - return "{!r}".format(self.__dict__) diff --git a/setup.py b/setup.py deleted file mode 100644 index a96ef63..0000000 --- a/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -from setuptools import find_packages, setup - -with open("README.md", "r") as readme_file: - readme = readme_file.read() - -setup( - name='serverlessworkflow.sdk', - packages=find_packages(include=['serverlessworkflow', 'serverlessworkflow.sdk']), - version='1.0.0', - description='Serverless Workflow Specification - Python SDK', - long_description=readme, - long_description_content_type="text/markdown", - url="https://serverlessworkflow.io/", - author='Serverless Workflow Contributors', - license='http://www.apache.org/licenses/LICENSE-2.0.txt', - install_requires=['pyyaml==6.0', "jsonschema==4.4.0", "requests", "pygraphviz==1.11", "transitions==0.9.2"], - setup_requires=['pytest-runner'], - tests_require=['pytest'], - test_suite='tests', -) diff --git a/submodules/specification b/submodules/specification new file mode 160000 index 0000000..6390951 --- /dev/null +++ b/submodules/specification @@ -0,0 +1 @@ +Subproject commit 63909511596ef1b3e6c7705ed4f8e6544076ef7e diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..8b9292a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Serverless Workflow SDK.""" diff --git a/tests/examples/advertise-listing.json b/tests/examples/advertise-listing.json deleted file mode 100644 index 80ac7d9..0000000 --- a/tests/examples/advertise-listing.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "advertise-listing", - "version": "0.1.0", - "specVersion": "0.8", - "start": "f8-advertise-listing", - "states": [ - { - "name": "f8-advertise-listing", - "type": "operation", - "actions": [ - { - "functionRef": "f8" - } - ], - "transition": "advertise-parallel" - }, - { - "name": "advertise-parallel", - "type": "parallel", - "branches": [ - { - "name": "send-sms", - "actions": [ - { - "functionRef": "f9" - } - ] - }, - { - "name": "tweet", - "actions": [ - { - "functionRef": "f10" - } - ] - }, - { - "name": "messenger-chatbot", - "actions": [ - { - "functionRef": "f11" - } - ] - }, - { - "name": "test", - "actions": [ - { - "subFlowRef": "second-subgraph" - } - ] - } - ], - "end": true - } - ] -} \ No newline at end of file diff --git a/tests/examples/applicantrequest.json b/tests/examples/applicantrequest.json deleted file mode 100644 index 8a05887..0000000 --- a/tests/examples/applicantrequest.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "id": "applicantrequest", - "name": "Applicant Request Decision Workflow", - "version": "1.0", - "description": "Determine if applicant request is valid", - "specVersion": "0.8", - "start": "CheckApplication", - "states": [ - { - "name": "CheckApplication", - "type": "switch", - "dataConditions": [ - { - "condition": "${ .applicants | .age >= 18 }", - "transition": "StartApplication" - }, - { - "condition": "${ .applicants | .age < 18 }", - "transition": "RejectApplication" - } - ], - "defaultCondition": { - "transition": "RejectApplication" - } - }, - { - "name": "StartApplication", - "type": "operation", - "actions": [ - { - "subFlowRef": "startApplicationWorkflowId" - } - ], - "end": true - }, - { - "name": "RejectApplication", - "type": "operation", - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "sendRejectionEmailFunction", - "arguments": { - "applicant": "${ .applicant }" - } - } - } - ], - "end": true - } - ], - "functions": [ - { - "name": "sendRejectionEmailFunction", - "operation": "http://myapis.org/applicationapi.json#emailRejection" - } - ] -} \ No newline at end of file diff --git a/tests/examples/booklending.json b/tests/examples/booklending.json deleted file mode 100644 index cc640fc..0000000 --- a/tests/examples/booklending.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "id": "booklending", - "name": "Book Lending Workflow", - "version": "1.0", - "specVersion": "0.8", - "start": "Book Lending Request", - "states": [ - { - "name": "Book Lending Request", - "type": "event", - "onEvents": [ - { - "eventRefs": [ - "Book Lending Request Event" - ] - } - ], - "transition": "Get Book Status" - }, - { - "name": "Get Book Status", - "type": "operation", - "actions": [ - { - "functionRef": { - "refName": "Get status for book", - "arguments": { - "bookid": "${ .book.id }" - } - } - } - ], - "transition": "Book Status Decision" - }, - { - "name": "Book Status Decision", - "type": "switch", - "dataConditions": [ - { - "name": "Book is on loan", - "condition": "${ .book.status == \"onloan\" }", - "transition": "Report Status To Lender" - }, - { - "name": "Check is available", - "condition": "${ .book.status == \"available\" }", - "transition": "Check Out Book" - } - ], - "defaultCondition": { - "end": true - } - }, - { - "name": "Report Status To Lender", - "type": "operation", - "actions": [ - { - "functionRef": { - "refName": "Send status to lender", - "arguments": { - "bookid": "${ .book.id }", - "message": "Book ${ .book.title } is already on loan" - } - } - } - ], - "transition": "Wait for Lender response" - }, - { - "name": "Wait for Lender response", - "type": "switch", - "eventConditions": [ - { - "name": "Hold Book", - "eventRef": "Hold Book Event", - "transition": "Request Hold" - }, - { - "name": "Decline Book Hold", - "eventRef": "Decline Hold Event", - "transition": "Cancel Request" - } - ], - "defaultCondition": { - "end": true - } - }, - { - "name": "Request Hold", - "type": "operation", - "actions": [ - { - "functionRef": { - "refName": "Request hold for lender", - "arguments": { - "bookid": "${ .book.id }", - "lender": "${ .lender }" - } - } - } - ], - "transition": "Sleep two weeks" - }, - { - "name": "Sleep two weeks", - "type": "sleep", - "duration": "PT2W", - "transition": "Get Book Status" - }, - { - "name": "Check Out Book", - "type": "operation", - "actions": [ - { - "functionRef": { - "refName": "Check out book with id", - "arguments": { - "bookid": "${ .book.id }" - } - } - }, - { - "functionRef": { - "refName": "Notify Lender for checkout", - "arguments": { - "bookid": "${ .book.id }", - "lender": "${ .lender }" - } - } - } - ], - "end": true - } - ], - "functions": "file://books/lending/functions.json", - "events": "file://books/lending/events.json" -} diff --git a/tests/examples/carauctionbids.json b/tests/examples/carauctionbids.json deleted file mode 100644 index 2588031..0000000 --- a/tests/examples/carauctionbids.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "id": "handleCarAuctionBid", - "name": "Car Auction Bidding Workflow", - "version": "1.0", - "description": "Store a single bid whole the car auction is active", - "specVersion": "0.8", - "start": { - "stateName": "StoreCarAuctionBid", - "schedule": "R/PT2H" - }, - "states": [ - { - "name": "StoreCarAuctionBid", - "type": "event", - "exclusive": true, - "onEvents": [ - { - "eventRefs": [ - "CarBidEvent" - ], - "actions": [ - { - "functionRef": { - "refName": "StoreBidFunction", - "arguments": { - "bid": "${ .bid }" - } - } - } - ] - } - ], - "end": true - } - ], - "functions": [ - { - "name": "StoreBidFunction", - "operation": "http://myapis.org/carauctionapi.json#storeBid" - } - ], - "events": [ - { - "name": "CarBidEvent", - "type": "carBidMadeType", - "source": "carBidEventSource" - } - ] -} \ No newline at end of file diff --git a/tests/examples/checkcarvitals.json b/tests/examples/checkcarvitals.json deleted file mode 100644 index af6ecd3..0000000 --- a/tests/examples/checkcarvitals.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "checkcarvitals", - "name": "Check Car Vitals Workflow", - "version": "1.0", - "specVersion": "0.8", - "start": "WhenCarIsOn", - "states": [ - { - "name": "WhenCarIsOn", - "type": "event", - "onEvents": [ - { - "eventRefs": [ - "CarTurnedOnEvent" - ] - } - ], - "transition": "DoCarVitalChecks" - }, - { - "name": "DoCarVitalChecks", - "type": "operation", - "actions": [ - { - "subFlowRef": "vitalscheck", - "sleep": { - "after": "PT1S" - } - } - ], - "transition": "CheckContinueVitalChecks" - }, - { - "name": "CheckContinueVitalChecks", - "type": "switch", - "eventConditions": [ - { - "name": "Car Turned Off Condition", - "eventRef": "CarTurnedOffEvent", - "end": true - } - ], - "defaultCondition": { - "transition": "DoCarVitalChecks" - } - } - ], - "events": [ - { - "name": "CarTurnedOnEvent", - "type": "car.events", - "source": "my/car" - }, - { - "name": "CarTurnedOffEvent", - "type": "car.events", - "source": "my/car" - } - ] -} diff --git a/tests/examples/graph.json b/tests/examples/graph.json deleted file mode 100644 index c34652d..0000000 --- a/tests/examples/graph.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "id": "graph", - "version": "0.1.0", - "specVersion": "0.8", - "description": "Real estate listing website", - "start": "entry-event", - "states": [ - { - "name": "entry-event", - "type": "event", - "onEvents": [ - { - "eventRefs": [ - "triggerEvent" - ], - "actions": [ - { - "functionRef": "process-event" - } - ] - } - ], - "stateDataFilter": { - "output": "${ .transformed }" - }, - "transition": "entry-decision" - }, - { - "name": "entry-decision", - "type": "switch", - "dataConditions": [ - { - "condition": "${ .\"postListing\" == true }", - "transition": "f1-upload-listing" - }, - { - "condition": "${ .\"submitDoc\" == true }", - "transition": "f2-upload-verification" - }, - { - "condition": "${ .\"submitClientInfo\" == true }", - "transition": "f3-upload-client" - } - ], - "defaultCondition": { - "end": true - } - }, - { - "name": "f1-upload-listing", - "type": "operation", - "actionMode": "parallel", - "actions": [ - { - "functionRef": "f1" - }, - { - "functionRef": "f2" - }, - { - "functionRef": { - "refName": "f5" - } - } - ], - "transition": "d1" - }, - { - "name": "d1", - "type": "operation", - "actions": [ - { - "eventRef": { - "triggerEventRef": "uploadPhoto", - "resultEventRef": "newPhoto" - } - } - ], - "transition": "f4-photo-verification" - }, - { - "name": "f4-photo-verification", - "type": "operation", - "actions": [ - { - "functionRef": "f4" - } - ], - "transition": "f5-image-correction" - }, - { - "name": "f5-image-correction", - "type": "operation", - "actions": [ - { - "functionRef": "f5" - } - ], - "transition": "f6-post-website" - }, - { - "name": "f6-post-website", - "type": "operation", - "actions": [ - { - "functionRef": "f6" - } - ], - "transition": "advertise-listing" - }, - { - "name": "advertise-listing", - "type": "operation", - "actions": [ - { - "subFlowRef": "advertise-listing" - } - ], - "end": true - }, - { - "name": "f2-upload-verification", - "type": "operation", - "actions": [ - { - "functionRef": "f2" - } - ], - "transition": "d2" - }, - { - "name": "f3-upload-client", - "type": "operation", - "actions": [ - { - "functionRef": "f3" - } - ], - "transition": "d2" - }, - { - "name": "d2", - "type": "operation", - "actions": [ - { - "eventRef": { - "triggerEventRef": "uploadInfo", - "resultEventRef": "newInfo" - } - } - ], - "transition": "new-info" - }, - { - "name": "new-info", - "type": "parallel", - "branches": [ - { - "name": "post-website", - "actions": [ - { - "functionRef": "f7" - }, - { - "functionRef": "f6" - } - ] - }, - { - "name": "advertise", - "actions": [ - { - "subFlowRef": "advertise-listing" - } - ] - } - ], - "end": true - } - ] -} \ No newline at end of file diff --git a/tests/examples/helloworld.json b/tests/examples/helloworld.json deleted file mode 100644 index 619af07..0000000 --- a/tests/examples/helloworld.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "helloworld", - "version": "1.0", - "specVersion": "0.8", - "name": "Hello World Workflow", - "description": "Inject Hello World", - "start": "Hello State", - "states": [ - { - "name": "Hello State", - "type": "inject", - "data": { - "result": "Hello World!" - }, - "end": true - } - ] -} \ No newline at end of file diff --git a/tests/examples/jobmonitoring.json b/tests/examples/jobmonitoring.json deleted file mode 100644 index 815b711..0000000 --- a/tests/examples/jobmonitoring.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "id": "jobmonitoring", - "name": "Job Monitoring", - "version": "1.0", - "description": "Monitor finished execution of a submitted job", - "specVersion": "0.8", - "start": "SubmitJob", - "states": [ - { - "name": "SubmitJob", - "type": "operation", - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "submitJob", - "arguments": { - "name": "${ .job.name }" - } - }, - "actionDataFilter": { - "results": "${ .jobuid }" - } - } - ], - "stateDataFilter": { - "output": "${ .jobuid }" - }, - "transition": "WaitForCompletion" - }, - { - "name": "WaitForCompletion", - "type": "sleep", - "duration": "PT5S", - "transition": "GetJobStatus" - }, - { - "name": "GetJobStatus", - "type": "operation", - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "checkJobStatus", - "arguments": { - "name": "${ .jobuid }" - } - }, - "actionDataFilter": { - "results": "${ .jobstatus }" - } - } - ], - "stateDataFilter": { - "output": "${ .jobstatus }" - }, - "transition": "DetermineCompletion" - }, - { - "name": "DetermineCompletion", - "type": "switch", - "dataConditions": [ - { - "condition": "${ .jobStatus == \"SUCCEEDED\" }", - "transition": "JobSucceeded" - }, - { - "condition": "${ .jobStatus == \"FAILED\" }", - "transition": "JobFailed" - } - ], - "defaultCondition": { - "transition": "WaitForCompletion" - } - }, - { - "name": "JobSucceeded", - "type": "operation", - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "reportJobSuceeded", - "arguments": { - "name": "${ .jobuid }" - } - } - } - ], - "end": true - }, - { - "name": "JobFailed", - "type": "operation", - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "reportJobFailed", - "arguments": { - "name": "${ .jobuid }" - } - } - } - ], - "end": true - } - ], - "functions": [ - { - "name": "submitJob", - "operation": "http://myapis.org/monitorapi.json#doSubmit" - }, - { - "name": "checkJobStatus", - "operation": "http://myapis.org/monitorapi.json#checkStatus" - }, - { - "name": "reportJobSuceeded", - "operation": "http://myapis.org/monitorapi.json#reportSucceeded" - }, - { - "name": "reportJobFailed", - "operation": "http://myapis.org/monitorapi.json#reportFailure" - } - ] -} \ No newline at end of file diff --git a/tests/examples/parallel.json b/tests/examples/parallel.json deleted file mode 100644 index 6282297..0000000 --- a/tests/examples/parallel.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "id": "parallelexec", - "name": "Parallel Execution Workflow", - "version": "1.0", - "description": "Executes two branches in parallel", - "specVersion": "0.8", - "start": "ParallelExec", - "states": [ - { - "name": "ParallelExec", - "type": "parallel", - "completionType": "allOf", - "branches": [ - { - "name": "ShortDelayBranch", - "actions": [ - { - "subFlowRef": "shortdelayworkflowid" - } - ] - }, - { - "name": "LongDelayBranch", - "actions": [ - { - "subFlowRef": "longdelayworkflowid" - } - ] - } - ], - "end": true - } - ] -} \ No newline at end of file diff --git a/tests/examples/provisionorder.json b/tests/examples/provisionorder.json deleted file mode 100644 index 2ef8d23..0000000 --- a/tests/examples/provisionorder.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "id": "provisionorders", - "name": "Provision Orders", - "version": "1.0", - "description": "Provision Orders and handle errors thrown", - "specVersion": "0.8", - "start": "ProvisionOrder", - "states": [ - { - "name": "ProvisionOrder", - "type": "operation", - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "provisionOrderFunction", - "arguments": { - "order": "${ .order }" - } - } - } - ], - "stateDataFilter": { - "output": "${ .exceptions }" - }, - "transition": "ApplyOrder", - "onErrors": [ - { - "errorRef": "Missing order id", - "transition": "MissingId" - }, - { - "errorRef": "Missing order item", - "transition": "MissingItem" - }, - { - "errorRef": "Missing order quantity", - "transition": "MissingQuantity" - } - ] - }, - { - "name": "MissingId", - "type": "operation", - "actions": [ - { - "subFlowRef": "handleMissingIdExceptionWorkflow" - } - ], - "end": true - }, - { - "name": "MissingItem", - "type": "operation", - "actions": [ - { - "subFlowRef": "handleMissingItemExceptionWorkflow" - } - ], - "end": true - }, - { - "name": "MissingQuantity", - "type": "operation", - "actions": [ - { - "subFlowRef": "handleMissingQuantityExceptionWorkflow" - } - ], - "end": true - }, - { - "name": "ApplyOrder", - "type": "operation", - "actions": [ - { - "subFlowRef": "applyOrderWorkflowId" - } - ], - "end": true - } - ], - "functions": [ - { - "name": "provisionOrderFunction", - "operation": "http://myapis.org/provisioningapi.json#doProvision" - } - ], - "errors": [ - { - "name": "Missing order id" - }, - { - "name": "Missing order item" - }, - { - "name": "Missing order quantity" - } - ] -} \ No newline at end of file diff --git a/tests/examples/second-subgraph.json b/tests/examples/second-subgraph.json deleted file mode 100644 index ef1c0aa..0000000 --- a/tests/examples/second-subgraph.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "id": "second-subgraph", - "version": "0.1.0", - "specVersion": "0.8", - "start": "test-switch", - "states": [ - { - "name": "test-switch", - "type": "switch", - "dataConditions": [ - { - "condition": "condition1", - "transition": "test" - }, - { - "condition": "condition2", - "transition": "test2" - } - ], - "defaultCondition": { - "end": true - } - }, - { - "name": "test", - "type": "parallel", - "branches": [ - { - "name": "send-sms", - "actions": [ - { - "functionRef": "f9" - } - ] - }, - { - "name": "tweet", - "actions": [ - { - "functionRef": "f10" - } - ] - }, - { - "name": "messenger-chatbot", - "actions": [ - { - "functionRef": "f11" - } - ] - } - ], - "end": true - }, - { - "name": "test2", - "type": "operation", - "actions": [ - { - "functionRef": "functionref" - } - ], - "end": true - } - ] -} diff --git a/tests/examples/sendcloudevent.json b/tests/examples/sendcloudevent.json deleted file mode 100644 index 11f5ac8..0000000 --- a/tests/examples/sendcloudevent.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "id": "sendcloudeventonprovision", - "name": "Send CloudEvent on provision completion", - "version": "1.0", - "specVersion": "0.8", - "start": "ProvisionOrdersState", - "states": [ - { - "name": "ProvisionOrdersState", - "type": "foreach", - "inputCollection": "${ .orders }", - "iterationParam": "singleorder", - "outputCollection": "${ .provisionedOrders }", - "actions": [ - { - "functionRef": { - "refName": "provisionOrderFunction", - "arguments": { - "order": "${ .singleorder }" - } - } - } - ], - "end": { - "produceEvents": [ - { - "eventRef": "provisioningCompleteEvent", - "data": "${ .provisionedOrders }" - } - ] - } - } - ], - "functions": [ - { - "name": "provisionOrderFunction", - "operation": "http://myapis.org/provisioning.json#doProvision" - } - ], - "events": [ - { - "kind": "produced", - "name": "provisioningCompleteEvent", - "type": "provisionCompleteType" - } - ] -} \ No newline at end of file diff --git a/tests/examples/solvemathproblems.json b/tests/examples/solvemathproblems.json deleted file mode 100644 index d525922..0000000 --- a/tests/examples/solvemathproblems.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "id": "solvemathproblems", - "name": "Solve Math Problems Workflow", - "version": "1.0", - "description": "Solve math problems", - "specVersion": "0.8", - "start": "Solve", - "states": [ - { - "name": "Solve", - "type": "foreach", - "inputCollection": "${ .expressions }", - "iterationParam": "singleexpression", - "outputCollection": "${ .results }", - "actions": [ - { - "functionRef": { - "refName": "solveMathExpressionFunction", - "arguments": { - "expression": "${ .singleexpression }" - } - } - } - ], - "stateDataFilter": { - "output": "${ .results }" - }, - "end": true - } - ], - "functions": [ - { - "name": "solveMathExpressionFunction", - "operation": "http://myapis.org/mapthapis.json#solveExpression" - } - ] -} \ No newline at end of file diff --git a/tests/serverlessworkflow/__init__.py b/tests/serverlessworkflow/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/serverlessworkflow/sdk/__init__.py b/tests/serverlessworkflow/sdk/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/serverlessworkflow/sdk/test_action.py b/tests/serverlessworkflow/sdk/test_action.py deleted file mode 100644 index db07340..0000000 --- a/tests/serverlessworkflow/sdk/test_action.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.action_data_filter import ActionDataFilter -from serverlessworkflow.sdk.function_ref import FunctionRef - - -class TestAction(unittest.TestCase): - - def test_load_function_ref(self): - action = Action(functionRef=FunctionRef(refName="refNameTest"), - retryableErrors=["err1", "err2"]) - self.assertTrue(isinstance(action.functionRef, FunctionRef)) - self.assertTrue(action.functionRef.refName, "refNameTest") - self.assertTrue(isinstance(action.retryableErrors, list)) - - def test_load_retryable_errors(self): - action_data = { - 'retryableErrors': ['err1', 'err2']} - action = Action(**action_data) - self.assertTrue(isinstance(action.retryableErrors, list)) - - def test_dinamic_load(self): - action_data = { - "functionRef": { - "refName": "greetingFunction", - "arguments": { - "name": "${ .person.name }" - } - }, - "actionDataFilter": { - "results": "${ .greeting }" - } - } - action = Action(**action_data) - self.assertTrue(isinstance(action.actionDataFilter, ActionDataFilter)) diff --git a/tests/serverlessworkflow/sdk/test_event_based_switch_state.py b/tests/serverlessworkflow/sdk/test_event_based_switch_state.py deleted file mode 100644 index 0bf41ae..0000000 --- a/tests/serverlessworkflow/sdk/test_event_based_switch_state.py +++ /dev/null @@ -1,16 +0,0 @@ -import unittest - -from serverlessworkflow.sdk.event_based_switch_state import EventBasedSwitchState -from serverlessworkflow.sdk.transition_event_condition import TransitionEventCondition - - -class TestEventBasedSwitchState(unittest.TestCase): - def test_programmatically_create_object(self): - event_based_switch_state = EventBasedSwitchState(eventConditions=[TransitionEventCondition( - name="Hold Book", - eventRef="Hold Book Event", - transition="Request Hold" - - )]) - - self.assertTrue(isinstance(event_based_switch_state.eventConditions[0], TransitionEventCondition)) diff --git a/tests/serverlessworkflow/sdk/test_hydrate.py b/tests/serverlessworkflow/sdk/test_hydrate.py deleted file mode 100644 index b541a58..0000000 --- a/tests/serverlessworkflow/sdk/test_hydrate.py +++ /dev/null @@ -1,34 +0,0 @@ -import unittest - -from serverlessworkflow.sdk.swf_base import ArrayTypeOf, ComplexTypeOf, UnionTypeOf, SimpleTypeOf - - -class AnyClass: - def __init__(self, **kwargs): - pass - - -class TestHydrateArrayOf(unittest.TestCase): - - def test_hydrate(self): - result_object = ArrayTypeOf(AnyClass).hydrate([{}, {}]) - self.assertTrue(isinstance(result_object[0], AnyClass)) - self.assertTrue(isinstance(result_object[1], AnyClass)) - - result_string = ArrayTypeOf(str).hydrate(["one", "two"]) - self.assertTrue(isinstance(result_string[0], str)) - self.assertTrue(isinstance(result_string[1], str)) - - -class TestHydrateUnionOfType(unittest.TestCase): - - def test_hydrate(self): - result_string = UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(AnyClass)]).hydrate("anyValue") - self.assertTrue(isinstance(result_string, str)) - - result_object = UnionTypeOf([SimpleTypeOf(str), ComplexTypeOf(AnyClass)]).hydrate({}) - self.assertTrue(isinstance(result_object, AnyClass)) - - result_array = UnionTypeOf([SimpleTypeOf(str), ArrayTypeOf(AnyClass)]).hydrate([{}, {}]) - self.assertTrue(isinstance(result_array, list)) - self.assertTrue(isinstance(result_array[0], AnyClass)) diff --git a/tests/serverlessworkflow/sdk/test_workflow.json b/tests/serverlessworkflow/sdk/test_workflow.json deleted file mode 100644 index 0a83319..0000000 --- a/tests/serverlessworkflow/sdk/test_workflow.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "id": "greeting", - "name": "Greeting Workflow", - "version": "1.0", - "description": "Greet Someone", - "specVersion": "0.8", - "start": "Greet", - "states": [ - { - "name": "Greet", - "type": "operation", - "actions": [ - { - "functionRef": { - "refName": "greetingFunction", - "arguments": { - "name": "${ .person.name }" - } - }, - "actionDataFilter": { - "results": "${ .greeting }" - } - } - ], - "end": true - } - ], - "functions": [ - { - "name": "greetingFunction", - "operation": "file://myapis/greetingapis.json#greeting" - } - ] -} \ No newline at end of file diff --git a/tests/serverlessworkflow/sdk/test_workflow.py b/tests/serverlessworkflow/sdk/test_workflow.py deleted file mode 100644 index 335d69d..0000000 --- a/tests/serverlessworkflow/sdk/test_workflow.py +++ /dev/null @@ -1,155 +0,0 @@ -import os -import unittest -from os import listdir - -from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.action_data_filter import ActionDataFilter -from serverlessworkflow.sdk.function import Function -from serverlessworkflow.sdk.function_ref import FunctionRef -from serverlessworkflow.sdk.operation_state import OperationState -from serverlessworkflow.sdk.workflow import Workflow - - -class TestWorkflow(unittest.TestCase): - workflow = Workflow( - id="greeting", - name="Greeting Workflow", - description="Greet Someone", - version='1.0', - specVersion='0.8', - start="Greet", - states=[ - OperationState( - name="Greet", - type="operation", - actions=[ - Action( - functionRef=FunctionRef( - refName="greetingFunction", - arguments={ - "name": "${ .person.name }" - } - ), - actionDataFilter=ActionDataFilter( - results="${ .greeting }" - ) - ) - ], - end=True - ) - ], - functions=[ - Function(name="greetingFunction", - operation="file://myapis/greetingapis.json#greeting") - ] - ) - - def test_workflow_to_json(self): - expected = """{ - "id": "greeting", - "name": "Greeting Workflow", - "version": "1.0", - "description": "Greet Someone", - "specVersion": "0.8", - "start": "Greet", - "states": [ - { - "name": "Greet", - "type": "operation", - "actions": [ - { - "functionRef": { - "refName": "greetingFunction", - "arguments": { - "name": "${ .person.name }" - } - }, - "actionDataFilter": { - "results": "${ .greeting }" - } - } - ], - "end": true - } - ], - "functions": [ - { - "name": "greetingFunction", - "operation": "file://myapis/greetingapis.json#greeting" - } - ] -}""" - - self.assertEqual(expected, self.workflow.to_json()) - - def test_workflow_to_yaml(self): - expected = """id: greeting -name: Greeting Workflow -version: '1.0' -description: Greet Someone -specVersion: '0.8' -start: Greet -states: -- name: Greet - type: operation - actions: - - functionRef: - refName: greetingFunction - arguments: - name: ${ .person.name } - actionDataFilter: - results: ${ .greeting } - end: true -functions: -- name: greetingFunction - operation: file://myapis/greetingapis.json#greeting -""" - self.assertEqual(expected, self.workflow.to_yaml()) - - def test_programmatically_create_workflow(self): - - self.assertEqual("greeting", self.workflow.id) - self.assertEqual("operation", self.workflow.states[0].type) - self.assertTrue(isinstance(self.workflow.states[0], OperationState)) - self.assertEqual(True, self.workflow.states[0].end) - self.assertTrue(isinstance(self.workflow.states[0].actions[0], Action)) - self.assertTrue(isinstance(self.workflow.states[0].actions[0].functionRef, FunctionRef)) - self.assertTrue(isinstance(self.workflow.functions[0], Function)) - - def test_workflow_from_source_json(self): - examples_dir = os.path.join(os.path.dirname(__file__), '../../examples') - examples = listdir(examples_dir) - self.assertEqual(len(examples), 13) - - for example in examples: - with self.subTest(f"test_{example}"): - with open(examples_dir + "/" + example, "r") as swf_file: - workflow = Workflow.from_source(swf_file.read()) - self.assertTrue(isinstance(workflow, Workflow)) - - def test_instance_workflow_class(self): - examples_dir = os.path.join(os.path.dirname(__file__), '../../examples') - examples = listdir(examples_dir) - self.assertEqual(len(examples), 13) - - for example in examples: - with self.subTest(f"test_{example}"): - with open(examples_dir + "/" + example, "r") as swf_file: - workflow = Workflow.from_source(swf_file.read()) - self.assertTrue(isinstance(workflow, Workflow)) - - def test_workflow_from_source_yaml(self): - wf_file = os.path.join(os.path.dirname(__file__), 'test_workflow.yaml') - self.assert_test_workflow_file(wf_file) - - def assert_test_workflow_file(self, wf_file): - with open(wf_file, "r") as swf_file: - workflow = Workflow.from_source(swf_file.read()) - - self.assertEqual("greeting", workflow.id) - self.assertEqual("operation", workflow.states[0].type) - self.assertEqual(True, workflow.states[0].end) - self.assertEqual('jq', workflow.expressionLang) - self.assertTrue(isinstance(workflow.states[0].actions[0], Action)) - self.assertTrue(isinstance(workflow.states[0].actions[0].functionRef, FunctionRef)) - self.assertTrue(isinstance(workflow.functions[0], Function)) diff --git a/tests/serverlessworkflow/sdk/test_workflow.yaml b/tests/serverlessworkflow/sdk/test_workflow.yaml deleted file mode 100644 index 1d19a4a..0000000 --- a/tests/serverlessworkflow/sdk/test_workflow.yaml +++ /dev/null @@ -1,20 +0,0 @@ -id: greeting -name: Greeting Workflow -version: '1.0' -description: Greet Someone -specVersion: '0.8' -start: Greet -states: - - name: Greet - type: operation - actions: - - functionRef: - refName: greetingFunction - arguments: - name: ${ .person.name } - actionDataFilter: - results: ${ .greeting } - end: true -functions: - - name: greetingFunction - operation: file://myapis/greetingapis.json#greeting \ No newline at end of file diff --git a/tests/serverlessworkflow/sdk/test_workflow_validator.py b/tests/serverlessworkflow/sdk/test_workflow_validator.py deleted file mode 100644 index f99de17..0000000 --- a/tests/serverlessworkflow/sdk/test_workflow_validator.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import unittest -from os import listdir - -from jsonschema.exceptions import ValidationError - -from serverlessworkflow.sdk.workflow import Workflow -from serverlessworkflow.sdk.workflow_validator import WorkflowValidator - - -class TestWorkflowValidator(unittest.TestCase): - - def test_validate_examples(self): - examples_dir = os.path.join(os.path.dirname(__file__), '../../examples') - examples = listdir(examples_dir) - self.assertEqual(len(examples), 13) - - for example in examples: - with self.subTest(f"test_{example}"): - with open(examples_dir + "/" + example, "r") as swf_file: - workflow = Workflow.from_source(swf_file.read()) - WorkflowValidator(workflow).validate() - - def test_valid_wf(self): - wf_file = os.path.join(os.path.dirname(__file__), '../../examples', 'applicantrequest.json') - with open(wf_file, "r") as swf_file: - workflow = Workflow.from_source(swf_file.read()) - - WorkflowValidator(workflow).validate() - - def test_invalid_wf(self): - wf_file = os.path.join(os.path.dirname(__file__), '../../examples', 'applicantrequest.json') - - with open(wf_file, "r") as swf_file: - workflow = Workflow.from_source(swf_file.read()) - workflow.specVersion = None - with self.assertRaises(ValidationError): - WorkflowValidator(Workflow(workflow)).validate() diff --git a/tests/specification/test_spec_examples.py b/tests/specification/test_spec_examples.py new file mode 100644 index 0000000..d0d7e4d --- /dev/null +++ b/tests/specification/test_spec_examples.py @@ -0,0 +1,1748 @@ +"""Tests for Serverless Workflow specification examples.""" + +from pathlib import Path + +import pytest + +from serverlessworkflow.sdk.base import ( + Duration, + Export, + Input, + Output, +) +from serverlessworkflow.sdk.call_tasks import ( + AsyncApiMessageConsumptionPolicy, + AsyncApiOutboundMessage, + AsyncApiServer, + AsyncApiSubscription, + CallAsyncApiArguments, + CallAsyncApiTask, + CallFunctionTask, + CallGrpcArguments, + CallGrpcTask, + CallHttpArguments, + CallHttpTask, + CallMcpArguments, + CallMcpTask, + CallOpenApiArguments, + CallOpenApiTask, + GrpcService, + McpStdioTransport, + McpTransport, +) +from serverlessworkflow.sdk.tasks import ( + CatchConfiguration, + ContainerConfiguration, + ContainerLifetime, + EmitConfiguration, + EmitTask, + ForConfiguration, + ForkConfiguration, + ForkTask, + ForTask, + ListenConfiguration, + ListenTask, + RaiseConfiguration, + RaiseTask, + RunConfiguration, + RunTask, + ScriptConfiguration, + SetTask, + ShellConfiguration, + SubscriptionIterator, + SwitchCase, + SwitchTask, + TryTask, + WaitTask, + WorkflowConfiguration, +) +from serverlessworkflow.sdk.workflow import Document, Schedule, Workflow + +SPEC_EXAMPLES_DIR = ( + Path(__file__).parent.parent.parent / "submodules" / "specification" / "examples" +) + + +@pytest.mark.spec_example +@pytest.mark.parametrize( + "example_file", list(SPEC_EXAMPLES_DIR.glob("*.yaml")), ids=lambda f: f.name +) +def test_spec_examples(example_file): + """Test that specification example files can be parsed and serialized correctly.""" + with open(example_file, encoding="utf-8") as f: + baseline_workflow = Workflow.from_yaml(f.read()) + + print(f"Testing workflow from {example_file.name}:") + + match example_file.name: + case "accumulate-room-readings.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="accumulate-room-readings", + version="0.1.0", + ), + do=[ + { + "consumeReading": ListenTask( + listen=ListenConfiguration( + to={ + "all": [ + { + "with": { + "source": "https://my.home.com/sensor", + "type": "my.home.sensors.temperature", + }, + "correlate": {"roomId": {"from": ".roomid"}}, + }, + { + "with": { + "source": "https://my.home.com/sensor", + "type": "my.home.sensors.humidity", + }, + "correlate": {"roomId": {"from": ".roomid"}}, + }, + ] + } + ), + output=Output(as_=".data.reading"), + ) + }, + { + "logReading": ForTask( + for_=ForConfiguration(each="reading", in_=".readings"), + do=[ + { + "callOrderService": CallOpenApiTask( + with_=CallOpenApiArguments( + document={ + "endpoint": "http://myorg.io/ordersservices.json" + }, + operationId="logreading", + ) + ) + } + ], + ) + }, + { + "generateReport": CallOpenApiTask( + with_=CallOpenApiArguments( + document={"endpoint": "http://myorg.io/ordersservices.json"}, + operationId="produceReport", + ) + ) + }, + ], + timeout={"after": {"hours": 1}}, + ) + case "authentication-bearer-uri-format.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="bearer-auth", # Note: filename doesn't match internal name + version="0.1.0", + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": {"bearer": {"token": "${ .token }"}}, + }, + ) + ) + } + ], + ) + case "authentication-bearer.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="bearer-auth-uri-format", # Note: filename doesn't match internal name + version="0.1.0", + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/1", + "authentication": {"bearer": {"token": "${ .token }"}}, + }, + ) + ) + } + ], + ) + case "authentication-oauth2-secret.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="oauth2-authentication", version="1.0.0" + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": {"oauth2": {"use": "mySecret"}}, + }, + ) + ) + } + ], + use={"secrets": ["mySecret"]}, + ) + case "authentication-oauth2.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="oauth2-authentication", version="0.1.0" + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": { + "oauth2": { + "authority": "http://keycloak/realms/fake-authority", + "endpoints": { + "token": "/auth/token", + "introspection": "/auth/introspect", + }, + "grant": "client_credentials", + "client": { + "id": "workflow-runtime-id", + "secret": "workflow-runtime-secret", + }, + } + }, + }, + ) + ) + } + ], + ) + case "authentication-oidc-secret.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="oidc-authentication", version="1.0.0" + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": {"oidc": {"use": "mySecret"}}, + }, + ) + ) + } + ], + use={"secrets": ["mySecret"]}, + ) + case "authentication-oidc.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="oidc-authentication", version="0.1.0" + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": { + "oidc": { + "authority": "http://keycloak/realms/fake-authority", + "grant": "client_credentials", + "client": { + "id": "workflow-runtime-id", + "secret": "workflow-runtime-secret", + }, + } + }, + }, + ) + ) + } + ], + ) + case "authentication-reusable.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="bearer-auth", version="0.1.0" + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint={ + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": {"use": "petStoreAuth"}, + }, + ) + ) + } + ], + use={"authentications": {"petStoreAuth": {"bearer": {"token": "${ .token }"}}}}, + ) + case "call-http-endpoint-interpolation-shorthand.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="call-http-shorthand-endpoint", + version="0.1.0", + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", endpoint="https://petstore.swagger.io/v2/pet/{petId}" + ) + ) + } + ], + ) + case "call-http-endpoint-interpolation.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="call-http-shorthand-endpoint", + version="0.1.0", + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint='${ "https://petstore.swagger.io/v2/pet/\\(.petId)" }', + headers={"content-type": "application/json"}, + ) + ) + } + ], + ) + case "call-http-query-headers-expressions.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="http-query-headers-expressions", + version="1.0.0", + ), + do=[ + { + "setQueryAndHeaders": SetTask( + set={ + "query": {"search": "${.searchQuery}"}, + "headers": {"Accept": "application/json"}, + } + ) + }, + { + "searchStarWarsCharacters": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://swapi.dev/api/people/", + headers="${.headers}", + query="${.query}", + ) + ) + }, + ], + input={ + "schema": { + "format": "json", + "document": { + "type": "object", + "required": ["searchQuery"], + "properties": {"searchQuery": {"type": "string"}}, + }, + } + }, + ) + case "call-http-query-parameters.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="http-query-params", version="1.0.0" + ), + do=[ + { + "searchStarWarsCharacters": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://swapi.dev/api/people/", + query={"search": "${.searchQuery}"}, + ) + ) + } + ], + input={ + "schema": { + "format": "json", + "document": { + "type": "object", + "required": ["searchQuery"], + "properties": {"searchQuery": {"type": "string"}}, + }, + } + }, + ) + case "call-http-redirect.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="http-query-params", version="1.0.0" + ), + do=[ + { + "searchStarWarsCharacters": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://swapi.dev/api/people/", + query={"search": "${.searchQuery}"}, + redirect=True, + ) + ) + } + ], + input={ + "schema": { + "format": "json", + "document": { + "type": "object", + "required": ["searchQuery"], + "properties": {"searchQuery": {"type": "string"}}, + }, + } + }, + ) + case "conditional-task.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="default", name="conditional-task", version="0.1.0" + ), + do=[ + { + "raiseErrorIfUnderage": RaiseTask( + if_=".customer.age < 18", + raise_=RaiseConfiguration( + error={ + "type": "https://superbet-casinos.com/customer/access-forbidden", + "status": 400, + "title": "Access Forbidden", + } + ), + then="end", + ) + }, + { + "placeBet": CallHttpTask( + with_=CallHttpArguments( + method="post", + endpoint="https://superbet-casinos.com/api/bet/on/football", + body={"customer": ".customer", "bet": ".bet"}, + ) + ) + }, + ], + ) + case "fork.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="fork-example", version="0.1.0" + ), + do=[ + { + "raiseAlarm": ForkTask( + fork=ForkConfiguration( + compete=True, + branches=[ + { + "callNurse": CallHttpTask( + with_=CallHttpArguments( + method="put", + endpoint="https://fake-hospital.com/api/v3/alert/nurses", + body={ + "patientId": "${ .patient.fullName }", + "room": "${ .room.number }", + }, + ) + ) + }, + { + "callDoctor": CallHttpTask( + with_=CallHttpArguments( + method="put", + endpoint="https://fake-hospital.com/api/v3/alert/doctor", + body={ + "patientId": "${ .patient.fullName }", + "room": "${ .room.number }", + }, + ) + ) + }, + ], + ) + ) + } + ], + ) + case "for.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="for-example", version="0.1.0" + ), + do=[ + { + "checkup": ForTask( + for_=ForConfiguration(each="pet", in_=".pets", at="index"), + while_=".vet != null", + do=[ + { + "waitForCheckup": ListenTask( + listen=ListenConfiguration( + to={ + "one": { + "with": { + "type": "com.fake.petclinic.pets.checkup.completed.v2" + } + } + } + ), + output=Output(as_='.pets + [{ "id": $pet.id }]'), + ) + } + ], + ) + } + ], + ) + case "listen-to-one.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="listen-to-one", version="0.1.0" + ), + do=[ + { + "waitForStartup": ListenTask( + listen=ListenConfiguration( + to={ + "one": { + "with": { + "type": "com.virtual-wf-powered-race.events.race.started.v1" + } + } + } + ) + ) + }, + { + "startup": CallHttpTask( + with_=CallHttpArguments( + method="post", + endpoint={ + "uri": "https://virtual-wf-powered-race.com/api/v4/cars/{carId}/start" + }, + ) + ) + }, + ], + ) + case "raise-inline.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="raise-not-implemented", version="0.1.0" + ), + do=[ + { + "notImplemented": RaiseTask( + raise_=RaiseConfiguration( + error={ + "type": "https://serverlessworkflow.io/errors/not-implemented", + "status": 500, + "title": "Not Implemented", + "detail": "${ \"The workflow '\\( $workflow.definition.document.name ):\\( $workflow.definition.document.version )' is a work in progress and cannot be run yet\" }", + } + ) + ) + } + ], + ) + case "do-multiple.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="call-http-shorthand-endpoint", + version="0.1.0", + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", endpoint="https://petstore.swagger.io/v2/pet/{petId}" + ) + ) + }, + { + "buyPet": CallHttpTask( + with_=CallHttpArguments( + method="put", + endpoint="https://petstore.swagger.io/v2/pet/{petId}", + body='${ . + { status: "sold" } }', + ) + ) + }, + ], + ) + case "do-single.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="call-http-shorthand-endpoint", + version="0.1.0", + ), + do=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", endpoint="https://petstore.swagger.io/v2/pet/{petId}" + ) + ) + } + ], + ) + case "emit.yaml": + new_workflow = Workflow( + document=Document(dsl="1.0.2", namespace="test", name="emit", version="0.1.0"), + do=[ + { + "emitEvent": EmitTask( + emit=EmitConfiguration( + event={ + "with": { + "source": "https://petstore.com", + "type": "com.petstore.order.placed.v1", + "data": { + "client": { + "firstName": "Cruella", + "lastName": "de Vil", + }, + "items": [{"breed": "dalmatian", "quantity": 101}], + }, + } + } + ) + ) + } + ], + ) + case "set.yaml": + new_workflow = Workflow( + document=Document(dsl="1.0.2", namespace="test", name="set", version="0.1.0"), + do=[{"initialize": SetTask(set={"startEvent": "${ $workflow.input[0] }"})}], + schedule={ + "on": { + "one": {"with": {"type": "io.serverlessworkflow.samples.events.trigger.v1"}} + } + }, + ) + case "set-expression.yaml": + new_workflow = Workflow( + document=Document(dsl="1.0.2", namespace="test", name="set", version="0.1.0"), + do=[{"initialize": SetTask(set="${ $workflow.input[0] }")}], + schedule={ + "on": { + "one": {"with": {"type": "io.serverlessworkflow.samples.events.trigger.v1"}} + } + }, + ) + case "wait-duration-inline.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="wait-duration-inline", version="0.1.0" + ), + do=[{"wait30Seconds": WaitTask(wait=Duration(seconds=30))}], + ) + case "wait-duration-iso8601.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="wait-duration-8601", version="0.1.0" + ), + do=[{"wait30Seconds": WaitTask(wait="PT30S")}], + ) + case "call-asyncapi-publish.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="bearer-auth", version="0.1.0" + ), + do=[ + { + "findPet": CallAsyncApiTask( + with_=CallAsyncApiArguments( + document={"endpoint": "https://fake.com/docs/asyncapi.json"}, + operation="findPetsByStatus", + server=AsyncApiServer(name="staging"), + message=AsyncApiOutboundMessage(payload={"petId": "${ .pet.id }"}), + authentication={"bearer": {"token": "${ .token }"}}, + ) + ) + } + ], + ) + case "call-asyncapi-subscribe-consume-amount.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="bearer-auth", version="0.1.0" + ), + do=[ + { + "getNotifications": CallAsyncApiTask( + with_=CallAsyncApiArguments( + document={"endpoint": "https://fake.com/docs/asyncapi.json"}, + operation="getNotifications", + protocol="ws", + subscription=AsyncApiSubscription( + filter="${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }", + consume=AsyncApiMessageConsumptionPolicy(amount=5), + ), + ) + ) + } + ], + ) + case "call-asyncapi-subscribe-consume-forever-foreach.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="bearer-auth", version="0.1.0" + ), + do=[ + { + "getNotifications": CallAsyncApiTask( + with_=CallAsyncApiArguments( + document={"endpoint": "https://fake.com/docs/asyncapi.json"}, + operation="getNotifications", + subscription=AsyncApiSubscription( + filter="${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }", + consume=AsyncApiMessageConsumptionPolicy(while_="${ true }"), + foreach=SubscriptionIterator( + item="message", + do=[ + { + "publishCloudEvent": EmitTask( + emit=EmitConfiguration( + event={ + "with": { + "source": "https://serverlessworkflow.io/samples", + "type": "io.serverlessworkflow.samples.asyncapi.message.consumed.v1", + "data": { + "message": "${ $message }" + }, + } + } + ) + ) + } + ], + ), + ), + ) + ) + } + ], + ) + case "call-asyncapi-subscribe-consume-until.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="bearer-auth", version="0.1.0" + ), + do=[ + { + "getNotifications": CallAsyncApiTask( + with_=CallAsyncApiArguments( + document={"endpoint": "https://fake.com/docs/asyncapi.json"}, + channel="/notifications", + subscription=AsyncApiSubscription( + filter="${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }", + consume=AsyncApiMessageConsumptionPolicy( + for_={"minutes": 30}, + until="${ ($context.consumedMessages | length) == 5 }", + ), + ), + ) + ) + } + ], + ) + case "call-asyncapi-subscribe-consume-while.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="bearer-auth", version="0.1.0" + ), + do=[ + { + "getNotifications": CallAsyncApiTask( + with_=CallAsyncApiArguments( + document={"endpoint": "https://fake.com/docs/asyncapi.json"}, + operation="getNotifications", + subscription=AsyncApiSubscription( + filter="${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }", + consume=AsyncApiMessageConsumptionPolicy( + while_="${ ($context.consumedMessages | length) < 5 }" + ), + ), + ) + ) + } + ], + ) + case "call-custom-function-cataloged.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="samples", + name="call-custom-function-cataloged", + version="0.1.0", + ), + do=[ + { + "log": CallFunctionTask( + call="https://raw.githubusercontent.com/serverlessworkflow/catalog/main/functions/log/1.0.0/function.yaml", + with_={ + "message": "Hello, world!", + "level": "information", + "timestamp": True, + "format": "{TIMESTAMP} [{LEVEL}] ({CONTEXT}): {MESSAGE}", + }, + ) + } + ], + ) + case "call-custom-function-inline.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="samples", + name="call-custom-function-inline", + version="0.1.0", + ), + do=[{"getPet": CallFunctionTask(call="getPetById", with_={"petId": 69})}], + use={ + "functions": { + "getPetById": { + "input": { + "schema": { + "document": { + "type": "object", + "properties": {"petId": {"type": "integer"}}, + "required": ["petId"], + } + } + }, + "call": "http", + "with": { + "method": "get", + "endpoint": "https://petstore.swagger.io/v2/pet/{petId}", + }, + } + } + }, + ) + case "call-grpc.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="grpc-example", version="0.1.0" + ), + do=[ + { + "greet": CallGrpcTask( + with_=CallGrpcArguments( + proto={"endpoint": "file://app/greet.proto"}, + service=GrpcService( + name="GreeterApi.Greeter", host="localhost", port=5011 + ), + method="SayHello", + arguments={"name": "${ .user.preferredDisplayName }"}, + ) + ) + } + ], + ) + case "call-mcp.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="mcp-example", version="0.1.0" + ), + do=[ + { + "publishMessageToSlack": CallMcpTask( + with_=CallMcpArguments( + method="tools/call", + parameters={ + "name": "conversations_add_message", + "arguments": { + "channel_id": "C1234567890", + "thread_ts": "1623456789.123456", + "payload": "Hello, world! :wave:", + "content_type": "text/markdown", + }, + }, + transport=McpTransport( + stdio=McpStdioTransport( + command="npx", + arguments=[ + "slack-mcp-serverr@latest", + "--transport", + "stdio", + ], + environment={ + "SLACK_MCP_XOXP_TOKEN": "xoxp-xv6Cv3jKqNW8esm5YnsftKwIzoQHUzAP" + }, + ) + ), + ) + ) + } + ], + ) + case "call-openapi.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="openapi-example", version="0.1.0" + ), + do=[ + { + "findPet": CallOpenApiTask( + with_=CallOpenApiArguments( + document={ + "endpoint": "https://petstore.swagger.io/v2/swagger.json" + }, + operationId="findPetsByStatus", + parameters={"status": "available"}, + ) + ) + } + ], + ) + case "call-openapi-redirect.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="openapi-example", version="0.1.0" + ), + do=[ + { + "findPet": CallOpenApiTask( + with_=CallOpenApiArguments( + document={ + "endpoint": "https://petstore.swagger.io/v2/swagger.json" + }, + operationId="findPetsByStatus", + parameters={"status": "available"}, + redirect=True, + ) + ) + } + ], + ) + case "listen-to-all.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="listen-to-all", version="0.1.0" + ), + do=[ + { + "callDoctor": ListenTask( + listen=ListenConfiguration( + to={ + "all": [ + { + "with": { + "type": "com.fake-hospital.vitals.measurements.temperature", + "data": "${ .temperature > 38 }", + } + }, + { + "with": { + "type": "com.fake-hospital.vitals.measurements.bpm", + "data": "${ .bpm < 60 or .bpm > 100 }", + } + }, + ] + } + ) + ) + } + ], + ) + case "listen-to-all read-envelope.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="test", + name="listen-to-all-read-envelope", + version="0.1.0", + ), + do=[ + { + "callDoctor": ListenTask( + listen=ListenConfiguration( + to={ + "all": [ + { + "with": { + "type": "com.fake-hospital.vitals.measurements.temperature", + "data": "${ .temperature > 38 }", + } + }, + { + "with": { + "type": "com.fake-hospital.vitals.measurements.bpm", + "data": "${ .bpm < 60 or .bpm > 100 }", + } + }, + ] + }, + read="envelope", + ) + ) + } + ], + ) + case "listen-to-any.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="listen-to-any", version="0.1.0" + ), + do=[{"callDoctor": ListenTask(listen=ListenConfiguration(to={"any": []}))}], + ) + case "listen-to-any-filter.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="listen-to-any-filter", version="0.1.0" + ), + do=[ + { + "callDoctor": ListenTask( + listen=ListenConfiguration( + to={ + "any": [ + { + "with": { + "type": "com.fake-hospital.vitals.measurements.temperature", + "data": "${ .temperature > 38 }", + } + }, + { + "with": { + "type": "com.fake-hospital.vitals.measurements.bpm", + "data": "${ .bpm < 60 or .bpm > 100 }", + } + }, + ] + } + ) + ) + } + ], + ) + case "listen-to-any-forever-foreach.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="test", + name="listen-to-any-while-foreach", + version="0.1.0", + ), + do=[ + { + "listenToGossips": ListenTask( + listen=ListenConfiguration(to={"any": [], "until": "${ false }"}), + foreach={ + "item": "event", + "at": "i", + "do": [ + { + "postToChatApi": CallHttpTask( + with_=CallHttpArguments( + method="post", + endpoint="https://fake-chat-api.com/room/{roomId}", + body={"event": "${ $event }"}, + ) + ) + } + ], + }, + ) + } + ], + ) + case "listen-to-any-until-condition.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="listen-to-any", version="0.1.0" + ), + do=[ + { + "callDoctor": ListenTask( + listen=ListenConfiguration( + to={"any": [], "until": "( . | length ) > 3"} + ) + ) + } + ], + ) + case "listen-to-any-until-consumed.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="listen-to-any", version="0.1.0" + ), + do=[ + { + "callDoctor": ListenTask( + listen=ListenConfiguration( + to={ + "any": [ + { + "with": { + "type": "com.fake-hospital.vitals.measurements.temperature", + "data": "${ .temperature > 38 }", + } + }, + { + "with": { + "type": "com.fake-hospital.vitals.measurements.bpm", + "data": "${ .bpm < 60 or .bpm > 100 }", + } + }, + ], + "until": { + "one": { + "with": { + "type": "com.fake-hospital.patient.checked-out" + } + } + }, + } + ) + ) + } + ], + ) + case "mock-service-extension.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="sample-workflow", version="0.1.0" + ), + do=[ + { + "callHttp": CallHttpTask( + with_=CallHttpArguments( + method="get", endpoint={"uri": "https://fake.com/sample"} + ) + ) + } + ], + use={ + "extensions": [ + { + "mockService": { + "extend": "call", + "when": '($task.with.endpoint != null and ($task.with.endpoint | startswith("https://mocked.service.com"))) or ($task.with.endpoint.uri != null and ($task.with.endpoint.uri | startswith("https://mocked.service.com")))', + "before": [ + { + "mockResponse": { + "set": { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "content": {"foo": {"bar": "baz"}}, + }, + "then": "exit", + } + } + ], + } + } + ] + }, + ) + case "raise-reusable.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="raise-not-implemented", version="0.1.0" + ), + do=[ + {"notImplemented": RaiseTask(raise_=RaiseConfiguration(error="notImplemented"))} + ], + use={ + "errors": { + "notImplemented": { + "type": "https://serverlessworkflow.io/errors/not-implemented", + "status": 500, + "title": "Not Implemented", + "detail": "${ \"The workflow '\\( $workflow.definition.document.name ):\\( $workflow.definition.document.version )' is a work in progress and cannot be run yet\" }", + } + } + }, + ) + case "run-container.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration(image="hello-world") + ) + ) + } + ], + ) + case "run-container-cleanup-always.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration( + image="hello-world", + lifetime=ContainerLifetime(cleanup="always"), + ) + ) + ) + } + ], + ) + case "run-container-cleanup-eventually.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration( + image="hello-world", + lifetime=ContainerLifetime( + cleanup="eventually", after={"minutes": 30} + ), + ) + ) + ) + } + ], + ) + case "run-container-stdin-and-arguments.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="test", + name="run-container-stdin-and-arguments", + version="0.1.0", + ), + do=[ + {"setInput": SetTask(set={"message": "Hello World"})}, + { + "runContainer": RunTask( + input=Input(from_="${ .message }"), + run=RunConfiguration( + container=ContainerConfiguration( + image="alpine", + command='input=$(cat)\necho "STDIN was: $input"\necho "ARGS are $1 $2"\n', + stdin="${ . }", + arguments=["Foo", "Bar"], + ) + ), + ) + }, + ], + ) + case "run-container-with-name.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container-with-name", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration( + image="hello-world", + name='${ "hello-\\(.workflow.document.name)-\\(.task.name)-\\(.workflow.id)" }', + ) + ) + ) + } + ], + ) + case "run-return-all.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration(image="hello-world"), return_="all" + ) + ) + } + ], + ) + case "run-return-code.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration(image="hello-world"), + return_="code", + ) + ) + } + ], + ) + case "run-return-none.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration(image="hello-world"), + return_="none", + ) + ) + } + ], + ) + case "run-return-stderr.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-container", version="0.1.0" + ), + do=[ + { + "runContainer": RunTask( + run=RunConfiguration( + container=ContainerConfiguration(image="hello-world"), + return_="stderr", + ) + ) + } + ], + ) + case "run-script-with-stdin-and-arguments.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="run-script-with-stdin-and-arguments", + version="1.0.0", + ), + do=[ + { + "runScript": RunTask( + run=RunConfiguration( + script=ScriptConfiguration( + language="javascript", + stdin="Hello Workflow", + environment={"foo": "bar"}, + arguments=["hello"], + code="// Reading Input from STDIN\nimport { readFileSync } from 'node:fs';\nconst stdin = readFileSync(process.stdin.fd, 'utf8');\nconsole.log('stdin > ', stdin) // Output: stdin > Hello Workflow\n\n// Reading from argv\nconst [_, __, arg] = process.argv;\nconsole.log('arg > ', arg) // Output: arg > hello\n\n// Reading from env\nconst foo = process.env.foo;\nconsole.log('env > ', foo) // Output: env > bar\n", + ) + ) + ) + } + ], + ) + case "run-shell-stdin-and-arguments.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", + namespace="examples", + name="run-shell-with-stdin-and-arguments", + version="1.0.0", + ), + do=[ + {"setInput": SetTask(set={"message": "Hello World"})}, + { + "runShell": RunTask( + input=Input(from_="${ .message }"), + run=RunConfiguration( + shell=ShellConfiguration( + stdin="${ . }", + command='input=$(cat)\necho "STDIN was: $input"\necho "ARGS are $1 $2"\n', + arguments=["Foo", "Bar"], + ) + ), + ) + }, + ], + ) + case "run-subflow.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="run-subflow", version="0.1.0" + ), + do=[ + { + "registerCustomer": RunTask( + run=RunConfiguration( + workflow=WorkflowConfiguration( + namespace="test", + name="register-customer", + version="0.1.0", + input={"customer": ".user"}, + ) + ) + ) + } + ], + ) + case "schedule-cron.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="cron-schedule", version="0.1.0" + ), + schedule=Schedule(cron="0 0 * * *"), + do=[ + { + "backup": CallHttpTask( + with_=CallHttpArguments( + method="post", endpoint="https://example.com/api/v1/backup/start" + ) + ) + } + ], + ) + case "schedule-event-driven.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="event-driven-schedule", version="0.1.0" + ), + schedule={ + "on": { + "one": { + "with": {"type": "com.example.hospital.events.patients.heartbeat.low"} + } + } + }, + do=[ + { + "callNurse": CallHttpTask( + with_=CallHttpArguments( + method="post", + endpoint="https://hospital.example.com/api/v1/notify", + body={ + "patientId": "${ $workflow.input[0].data.patient.id }", + "patientName": "${ $workflow.input[0].data.patient.name }", + "roomNumber": "${ $workflow.input[0].data.patient.room.number }", + "vitals": { + "heartRate": "${ $workflow.input[0].data.patient.vitals.bpm }", + "timestamp": "${ $workflow.input[0].data.timestamp }", + }, + "message": "Alert: Patient's heartbeat is critically low. Immediate attention required.", + }, + ) + ) + } + ], + ) + case "star-wars-homeworld.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="examples", name="star-wars-homeplanet", version="1.0.0" + ), + input={ + "schema": { + "format": "json", + "document": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "The id of the star wars character to get", + "minimum": 1, + } + }, + }, + } + }, + do=[ + { + "getStarWarsCharacter": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://swapi.dev/api/people/{id}", + output="response", + ), + export=Export(as_={"homeworld": "${ .content.homeworld }"}), + ) + }, + { + "getStarWarsHomeworld": CallHttpTask( + with_=CallHttpArguments( + method="get", endpoint="${ $context.homeworld }" + ) + ) + }, + ], + ) + case "switch-then-string.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="test", name="sample-workflow", version="0.1.0" + ), + do=[ + { + "processOrder": SwitchTask( + switch=[ + { + "case1": SwitchCase( + when='.orderType == "electronic"', + then="processElectronicOrder", + ) + }, + { + "case2": SwitchCase( + when='.orderType == "physical"', then="processPhysicalOrder" + ) + }, + {"default": SwitchCase(then="handleUnknownOrderType")}, + ] + ) + }, + { + "processElectronicOrder": SetTask( + set={"validate": True, "status": "fulfilled"}, then="exit" + ) + }, + { + "processPhysicalOrder": SetTask( + set={"inventory": "clear", "items": 1, "address": "Elmer St"}, + then="exit", + ) + }, + { + "handleUnknownOrderType": SetTask( + set={"log": "warn", "message": "something's wrong"} + ) + }, + ], + ) + case "try-catch.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="default", name="try-catch", version="0.1.0" + ), + do=[ + { + "tryGetPet": TryTask( + try_=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://petstore.swagger.io/v2/pet/{petId}", + ) + ) + } + ], + catch=CatchConfiguration( + errors={ + "with": { + "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication", + "status": 404, + } + } + ), + ) + } + ], + ) + case "try-catch-retry-inline.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="default", name="try-catch-retry", version="0.1.0" + ), + do=[ + { + "tryGetPet": TryTask( + try_=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://petstore.swagger.io/v2/pet/{petId}", + ) + ) + } + ], + catch=CatchConfiguration( + errors={ + "with": { + "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication", + "status": 503, + } + }, + retry={ + "delay": {"seconds": 3}, + "backoff": {"exponential": {}}, + "limit": {"attempt": {"count": 5}}, + }, + ), + ) + } + ], + ) + case "try-catch-retry-reusable.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="default", name="try-catch-retry", version="0.1.0" + ), + do=[ + { + "tryGetPet": TryTask( + try_=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://petstore.swagger.io/v2/pet/{petId}", + ) + ) + } + ], + catch=CatchConfiguration( + errors={ + "with": { + "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication", + "status": 503, + } + }, + retry="default", + ), + ) + } + ], + use={ + "retries": { + "default": { + "delay": {"seconds": 3}, + "backoff": {"exponential": {}}, + "limit": {"attempt": {"count": 5}}, + } + } + }, + ) + case "try-catch-then.yaml": + new_workflow = Workflow( + document=Document( + dsl="1.0.2", namespace="default", name="try-catch", version="0.1.0" + ), + do=[ + { + "tryGetPet": TryTask( + try_=[ + { + "getPet": CallHttpTask( + with_=CallHttpArguments( + method="get", + endpoint="https://petstore.swagger.io/v2/pet/{petId}", + ) + ) + } + ], + catch=CatchConfiguration( + errors={ + "with": { + "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication", + "status": 404, + } + }, + as_="error", + do=[ + { + "notifySupport": EmitTask( + emit=EmitConfiguration( + event={ + "with": { + "source": "https://petstore.swagger.io", + "type": "io.swagger.petstore.events.pets.not-found.v1", + "data": "${ $error }", + } + } + ) + ) + }, + { + "setError": SetTask( + set={"error": "$error"}, + export=Export(as_="$context + { error: $error }"), + ) + }, + ], + ), + ) + }, + { + "buyPet": CallHttpTask( + if_="$context.error == null", + with_=CallHttpArguments( + method="put", + endpoint="https://petstore.swagger.io/v2/pet/{petId}", + body='${ . + { status: "sold" } }', + ), + ) + }, + ], + ) + case _: + print("No specific test logic for this example.") + # fail + raise AssertionError(f"No test logic defined for {example_file.name}") + + # Compare serialized versions for better diff output + baseline_serialized = baseline_workflow.serialize() + new_serialized = new_workflow.serialize() + + if baseline_serialized != new_serialized: + import json + + print("\n=== BASELINE (from YAML) ===") + print(json.dumps(baseline_serialized, indent=2, default=str)) + print("\n=== NEW (constructed) ===") + print(json.dumps(new_serialized, indent=2, default=str)) + + assert baseline_serialized == new_serialized diff --git a/tests/specification/test_spec_examples_validations.py b/tests/specification/test_spec_examples_validations.py new file mode 100644 index 0000000..aa57ef6 --- /dev/null +++ b/tests/specification/test_spec_examples_validations.py @@ -0,0 +1,31 @@ +"""Validation tests for Serverless Workflow specification examples.""" + +from pathlib import Path + +import pytest + +from serverlessworkflow.sdk.workflow import Workflow + +SPEC_EXAMPLES_DIR = ( + Path(__file__).parent.parent.parent / "submodules" / "specification" / "examples" +) + + +@pytest.mark.spec_example +@pytest.mark.parametrize("example_file", list(SPEC_EXAMPLES_DIR.glob("*.yaml"))) +def test_spec_example_validations(example_file): + """Test that SDK can parse and validate spec examples.""" + # Parse the example using v1 SDK + with open(example_file, encoding="utf-8") as f: + workflow = Workflow.from_yaml(f.read()) + + # Verify basic structure was parsed correctly + assert workflow.document is not None + assert workflow.do is not None + assert len(workflow.do) > 0 + + # Test round-trip: YAML -> Workflow -> YAML -> Workflow + yaml_output = workflow.to_yaml() + workflow2 = Workflow.from_yaml(yaml_output) + + assert workflow == workflow2 diff --git a/tests/visualization/__init__.py b/tests/visualization/__init__.py new file mode 100644 index 0000000..ff27c99 --- /dev/null +++ b/tests/visualization/__init__.py @@ -0,0 +1 @@ +"""Docstring for tests.visualization.""" diff --git a/tests/visualization/fixtures/accumulate-room-readings.dot b/tests/visualization/fixtures/accumulate-room-readings.dot new file mode 100644 index 0000000..ad6483b --- /dev/null +++ b/tests/visualization/fixtures/accumulate-room-readings.dot @@ -0,0 +1,23 @@ +digraph "accumulate-room-readings" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +consumeReading [label="consumeReading\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +generateReport [label="generateReport", shape=box, style=rounded]; +logReading [label="logReading\nfor loop", shape=hexagon, style=filled, fillcolor=lightblue]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callOrderService -> generateReport; +callOrderService -> logReading [label="loop", style=dotted]; +consumeReading -> logReading; +generateReport -> end; +logReading -> callOrderService; +start -> consumeReading; +subgraph cluster_cluster_1 { +style=dashed; +labelloc=b; +callOrderService [label="callOrderService", shape=box, style=rounded]; +} +} diff --git a/tests/visualization/fixtures/authentication-bearer-uri-format.dot b/tests/visualization/fixtures/authentication-bearer-uri-format.dot new file mode 100644 index 0000000..5bc3817 --- /dev/null +++ b/tests/visualization/fixtures/authentication-bearer-uri-format.dot @@ -0,0 +1,14 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/{petId}', 'authentication': {'bearer': {'token': '${ .token }'}}}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/authentication-bearer.dot b/tests/visualization/fixtures/authentication-bearer.dot new file mode 100644 index 0000000..9f5563a --- /dev/null +++ b/tests/visualization/fixtures/authentication-bearer.dot @@ -0,0 +1,12 @@ +digraph "bearer-auth-uri-format" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/1', 'authentication': {'bearer': {'token': '${ .token }'}}}", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +start -> getPet; +} diff --git a/tests/visualization/fixtures/authentication-oauth2-secret.dot b/tests/visualization/fixtures/authentication-oauth2-secret.dot new file mode 100644 index 0000000..d353c68 --- /dev/null +++ b/tests/visualization/fixtures/authentication-oauth2-secret.dot @@ -0,0 +1,14 @@ +digraph "oauth2-authentication" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/{petId}', 'authentication': {'oauth2': {'use': 'mySecret'}}}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/authentication-oauth2.dot b/tests/visualization/fixtures/authentication-oauth2.dot new file mode 100644 index 0000000..b3c225c --- /dev/null +++ b/tests/visualization/fixtures/authentication-oauth2.dot @@ -0,0 +1,14 @@ +digraph "oauth2-authentication" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/{petId}', 'authentication': {'oauth2': {'authority': 'http://keycloak/realms/fake-authority', 'endpoints': {'token': '/auth/token', 'introspection': '/auth/introspect'}, 'grant': 'client_credentials', 'client': {'id': 'workflow-runtime-id', 'secret': 'workflow-runtime-secret'}}}}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/authentication-oidc-secret.dot b/tests/visualization/fixtures/authentication-oidc-secret.dot new file mode 100644 index 0000000..5c23976 --- /dev/null +++ b/tests/visualization/fixtures/authentication-oidc-secret.dot @@ -0,0 +1,14 @@ +digraph "oidc-authentication" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/{petId}', 'authentication': {'oidc': {'use': 'mySecret'}}}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/authentication-oidc.dot b/tests/visualization/fixtures/authentication-oidc.dot new file mode 100644 index 0000000..b1e5222 --- /dev/null +++ b/tests/visualization/fixtures/authentication-oidc.dot @@ -0,0 +1,14 @@ +digraph "oidc-authentication" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/{petId}', 'authentication': {'oidc': {'authority': 'http://keycloak/realms/fake-authority', 'grant': 'client_credentials', 'client': {'id': 'workflow-runtime-id', 'secret': 'workflow-runtime-secret'}}}}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/authentication-reusable.dot b/tests/visualization/fixtures/authentication-reusable.dot new file mode 100644 index 0000000..4e99402 --- /dev/null +++ b/tests/visualization/fixtures/authentication-reusable.dot @@ -0,0 +1,14 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET {'uri': 'https://petstore.swagger.io/v2/pet/{petId}', 'authentication': {'use': 'petStoreAuth'}}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/call-asyncapi-publish.dot b/tests/visualization/fixtures/call-asyncapi-publish.dot new file mode 100644 index 0000000..2e5e8d6 --- /dev/null +++ b/tests/visualization/fixtures/call-asyncapi-publish.dot @@ -0,0 +1,12 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +findPet [label="findPet\ncall: asyncapi\nfindPetsByStatus", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +findPet -> end; +start -> findPet; +} diff --git a/tests/visualization/fixtures/call-asyncapi-subscribe-consume-amount.dot b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-amount.dot new file mode 100644 index 0000000..3316568 --- /dev/null +++ b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-amount.dot @@ -0,0 +1,12 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getNotifications [label="getNotifications\ncall: asyncapi\ngetNotifications", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getNotifications -> end; +start -> getNotifications; +} diff --git a/tests/visualization/fixtures/call-asyncapi-subscribe-consume-forever-foreach.dot b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-forever-foreach.dot new file mode 100644 index 0000000..24f8e7f --- /dev/null +++ b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-forever-foreach.dot @@ -0,0 +1,18 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getNotifications [label="getNotifications\ncall: asyncapi\ngetNotifications", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getNotifications -> publishCloudEvent [label="foreach"]; +publishCloudEvent -> end; +start -> getNotifications; +subgraph cluster_cluster_1 { +style=dashed; +labelloc=b; +publishCloudEvent [label="publishCloudEvent", shape=box, style=rounded]; +} +} diff --git a/tests/visualization/fixtures/call-asyncapi-subscribe-consume-until.dot b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-until.dot new file mode 100644 index 0000000..f617759 --- /dev/null +++ b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-until.dot @@ -0,0 +1,12 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getNotifications [label="getNotifications\ncall: asyncapi", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getNotifications -> end; +start -> getNotifications; +} diff --git a/tests/visualization/fixtures/call-asyncapi-subscribe-consume-while.dot b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-while.dot new file mode 100644 index 0000000..3316568 --- /dev/null +++ b/tests/visualization/fixtures/call-asyncapi-subscribe-consume-while.dot @@ -0,0 +1,12 @@ +digraph "bearer-auth" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getNotifications [label="getNotifications\ncall: asyncapi\ngetNotifications", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getNotifications -> end; +start -> getNotifications; +} diff --git a/tests/visualization/fixtures/call-custom-function-cataloged.dot b/tests/visualization/fixtures/call-custom-function-cataloged.dot new file mode 100644 index 0000000..8d29767 --- /dev/null +++ b/tests/visualization/fixtures/call-custom-function-cataloged.dot @@ -0,0 +1,12 @@ +digraph "call-custom-function-cataloged" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +log [label="log", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +log -> end; +start -> log; +} diff --git a/tests/visualization/fixtures/call-custom-function-inline.dot b/tests/visualization/fixtures/call-custom-function-inline.dot new file mode 100644 index 0000000..71dae00 --- /dev/null +++ b/tests/visualization/fixtures/call-custom-function-inline.dot @@ -0,0 +1,12 @@ +digraph "call-custom-function-inline" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +start -> getPet; +} diff --git a/tests/visualization/fixtures/call-grpc.dot b/tests/visualization/fixtures/call-grpc.dot new file mode 100644 index 0000000..7b9ffdb --- /dev/null +++ b/tests/visualization/fixtures/call-grpc.dot @@ -0,0 +1,12 @@ +digraph "grpc-example" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +greet [label="greet", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +greet -> end; +start -> greet; +} diff --git a/tests/visualization/fixtures/call-http-endpoint-interpolation-shorthand.dot b/tests/visualization/fixtures/call-http-endpoint-interpolation-shorthand.dot new file mode 100644 index 0000000..d348429 --- /dev/null +++ b/tests/visualization/fixtures/call-http-endpoint-interpolation-shorthand.dot @@ -0,0 +1,14 @@ +digraph "call-http-shorthand-endpoint" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET https://petstore.swagger.io/v2/pet/{petId}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/call-http-endpoint-interpolation.dot b/tests/visualization/fixtures/call-http-endpoint-interpolation.dot new file mode 100644 index 0000000..fde12f8 --- /dev/null +++ b/tests/visualization/fixtures/call-http-endpoint-interpolation.dot @@ -0,0 +1,12 @@ +digraph "call-http-shorthand-endpoint" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET ${ "https://petstore.swagger.io/v2/pet/\(.petId)" }", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +start -> getPet; +} diff --git a/tests/visualization/fixtures/call-http-query-headers-expressions.dot b/tests/visualization/fixtures/call-http-query-headers-expressions.dot new file mode 100644 index 0000000..939570d --- /dev/null +++ b/tests/visualization/fixtures/call-http-query-headers-expressions.dot @@ -0,0 +1,14 @@ +digraph "http-query-headers-expressions" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +searchStarWarsCharacters [label="searchStarWarsCharacters\ncall: http\nGET https://swapi.dev/api/people/", shape=box, style=rounded]; +setQueryAndHeaders [label="setQueryAndHeaders\nset", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +searchStarWarsCharacters -> end; +setQueryAndHeaders -> searchStarWarsCharacters; +start -> setQueryAndHeaders; +} diff --git a/tests/visualization/fixtures/call-http-query-parameters.dot b/tests/visualization/fixtures/call-http-query-parameters.dot new file mode 100644 index 0000000..ad82dde --- /dev/null +++ b/tests/visualization/fixtures/call-http-query-parameters.dot @@ -0,0 +1,12 @@ +digraph "http-query-params" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +searchStarWarsCharacters [label="searchStarWarsCharacters\ncall: http\nGET https://swapi.dev/api/people/", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +searchStarWarsCharacters -> end; +start -> searchStarWarsCharacters; +} diff --git a/tests/visualization/fixtures/call-http-redirect.dot b/tests/visualization/fixtures/call-http-redirect.dot new file mode 100644 index 0000000..ad82dde --- /dev/null +++ b/tests/visualization/fixtures/call-http-redirect.dot @@ -0,0 +1,12 @@ +digraph "http-query-params" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +searchStarWarsCharacters [label="searchStarWarsCharacters\ncall: http\nGET https://swapi.dev/api/people/", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +searchStarWarsCharacters -> end; +start -> searchStarWarsCharacters; +} diff --git a/tests/visualization/fixtures/call-mcp.dot b/tests/visualization/fixtures/call-mcp.dot new file mode 100644 index 0000000..d83e39e --- /dev/null +++ b/tests/visualization/fixtures/call-mcp.dot @@ -0,0 +1,12 @@ +digraph "mcp-example" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +publishMessageToSlack [label="publishMessageToSlack", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +publishMessageToSlack -> end; +start -> publishMessageToSlack; +} diff --git a/tests/visualization/fixtures/call-openapi-redirect.dot b/tests/visualization/fixtures/call-openapi-redirect.dot new file mode 100644 index 0000000..1970734 --- /dev/null +++ b/tests/visualization/fixtures/call-openapi-redirect.dot @@ -0,0 +1,12 @@ +digraph "openapi-example" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +findPet [label="findPet", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +findPet -> end; +start -> findPet; +} diff --git a/tests/visualization/fixtures/call-openapi.dot b/tests/visualization/fixtures/call-openapi.dot new file mode 100644 index 0000000..1970734 --- /dev/null +++ b/tests/visualization/fixtures/call-openapi.dot @@ -0,0 +1,12 @@ +digraph "openapi-example" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +findPet [label="findPet", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +findPet -> end; +start -> findPet; +} diff --git a/tests/visualization/fixtures/conditional-task.dot b/tests/visualization/fixtures/conditional-task.dot new file mode 100644 index 0000000..450d8e5 --- /dev/null +++ b/tests/visualization/fixtures/conditional-task.dot @@ -0,0 +1,14 @@ +digraph "conditional-task" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +placeBet [label="placeBet\ncall: http\nPOST https://superbet-casinos.com/api/bet/on/football", shape=box, style=rounded]; +raiseErrorIfUnderage [label="raiseErrorIfUnderage\nraise", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +placeBet -> end; +raiseErrorIfUnderage -> placeBet; +start -> raiseErrorIfUnderage; +} diff --git a/tests/visualization/fixtures/do-multiple.dot b/tests/visualization/fixtures/do-multiple.dot new file mode 100644 index 0000000..a1a553f --- /dev/null +++ b/tests/visualization/fixtures/do-multiple.dot @@ -0,0 +1,17 @@ +digraph "call-http-shorthand-endpoint" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +buyPet [label="buyPet\ncall: http\nPUT https://petstore.swagger.io/v2/pet/{petId}", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET https://petstore.swagger.io/v2/pet/{petId}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +buyPet -> end; +getPet -> buyPet; +petId -> buyPet [style=dashed, xlabel="input"]; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/do-single.dot b/tests/visualization/fixtures/do-single.dot new file mode 100644 index 0000000..d348429 --- /dev/null +++ b/tests/visualization/fixtures/do-single.dot @@ -0,0 +1,14 @@ +digraph "call-http-shorthand-endpoint" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getPet [label="getPet\ncall: http\nGET https://petstore.swagger.io/v2/pet/{petId}", shape=box, style=rounded]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getPet -> end; +petId -> getPet [style=dashed, xlabel="input"]; +start -> getPet; +} diff --git a/tests/visualization/fixtures/emit.dot b/tests/visualization/fixtures/emit.dot new file mode 100644 index 0000000..6b070b4 --- /dev/null +++ b/tests/visualization/fixtures/emit.dot @@ -0,0 +1,12 @@ +digraph emit { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +emitEvent [label="emitEvent\nemit", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +emitEvent -> end; +start -> emitEvent; +} diff --git a/tests/visualization/fixtures/for.dot b/tests/visualization/fixtures/for.dot new file mode 100644 index 0000000..13d346e --- /dev/null +++ b/tests/visualization/fixtures/for.dot @@ -0,0 +1,19 @@ +digraph "for-example" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +checkup [label="checkup\nfor loop", shape=hexagon, style=filled, fillcolor=lightblue]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +checkup -> waitForCheckup; +start -> checkup; +waitForCheckup -> checkup [label="loop", style=dotted]; +waitForCheckup -> end; +subgraph cluster_cluster_1 { +style=dashed; +labelloc=b; +waitForCheckup [label="waitForCheckup", shape=box, style=rounded]; +} +} diff --git a/tests/visualization/fixtures/fork.dot b/tests/visualization/fixtures/fork.dot new file mode 100644 index 0000000..28b121b --- /dev/null +++ b/tests/visualization/fixtures/fork.dot @@ -0,0 +1,21 @@ +digraph "fork-example" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +raiseAlarm [label="raiseAlarm\nfork", shape=diamond, style=filled, fillcolor=lightgreen]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +callNurse -> end; +raiseAlarm -> callDoctor; +raiseAlarm -> callNurse; +start -> raiseAlarm; +subgraph cluster_cluster_1 { +style=dashed; +labelloc=b; +callDoctor [label="callDoctor", shape=box, style=rounded]; +callNurse [label="callNurse", shape=box, style=rounded]; +} +} diff --git a/tests/visualization/fixtures/listen-to-all read-envelope.dot b/tests/visualization/fixtures/listen-to-all read-envelope.dot new file mode 100644 index 0000000..b391af1 --- /dev/null +++ b/tests/visualization/fixtures/listen-to-all read-envelope.dot @@ -0,0 +1,12 @@ +digraph "listen-to-all-read-envelope" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callDoctor [label="callDoctor\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +start -> callDoctor; +} diff --git a/tests/visualization/fixtures/listen-to-all.dot b/tests/visualization/fixtures/listen-to-all.dot new file mode 100644 index 0000000..395ead1 --- /dev/null +++ b/tests/visualization/fixtures/listen-to-all.dot @@ -0,0 +1,12 @@ +digraph "listen-to-all" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callDoctor [label="callDoctor\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +start -> callDoctor; +} diff --git a/tests/visualization/fixtures/listen-to-any-filter.dot b/tests/visualization/fixtures/listen-to-any-filter.dot new file mode 100644 index 0000000..1021748 --- /dev/null +++ b/tests/visualization/fixtures/listen-to-any-filter.dot @@ -0,0 +1,12 @@ +digraph "listen-to-any-filter" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callDoctor [label="callDoctor\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +start -> callDoctor; +} diff --git a/tests/visualization/fixtures/listen-to-any-forever-foreach.dot b/tests/visualization/fixtures/listen-to-any-forever-foreach.dot new file mode 100644 index 0000000..40deade --- /dev/null +++ b/tests/visualization/fixtures/listen-to-any-forever-foreach.dot @@ -0,0 +1,12 @@ +digraph "listen-to-any-while-foreach" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +listenToGossips [label="listenToGossips\nlisten", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +listenToGossips -> end; +start -> listenToGossips; +} diff --git a/tests/visualization/fixtures/listen-to-any-until-condition.dot b/tests/visualization/fixtures/listen-to-any-until-condition.dot new file mode 100644 index 0000000..8478a5b --- /dev/null +++ b/tests/visualization/fixtures/listen-to-any-until-condition.dot @@ -0,0 +1,12 @@ +digraph "listen-to-any" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callDoctor [label="callDoctor\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +start -> callDoctor; +} diff --git a/tests/visualization/fixtures/listen-to-any-until-consumed.dot b/tests/visualization/fixtures/listen-to-any-until-consumed.dot new file mode 100644 index 0000000..8478a5b --- /dev/null +++ b/tests/visualization/fixtures/listen-to-any-until-consumed.dot @@ -0,0 +1,12 @@ +digraph "listen-to-any" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callDoctor [label="callDoctor\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +start -> callDoctor; +} diff --git a/tests/visualization/fixtures/listen-to-any.dot b/tests/visualization/fixtures/listen-to-any.dot new file mode 100644 index 0000000..8478a5b --- /dev/null +++ b/tests/visualization/fixtures/listen-to-any.dot @@ -0,0 +1,12 @@ +digraph "listen-to-any" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callDoctor [label="callDoctor\nlisten", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callDoctor -> end; +start -> callDoctor; +} diff --git a/tests/visualization/fixtures/listen-to-one.dot b/tests/visualization/fixtures/listen-to-one.dot new file mode 100644 index 0000000..758ea19 --- /dev/null +++ b/tests/visualization/fixtures/listen-to-one.dot @@ -0,0 +1,16 @@ +digraph "listen-to-one" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +carId [label="Input: carId", shape=box, style=filled, fillcolor=lightyellow]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +startup [label="startup\ncall: http\nPOST {'uri': 'https://virtual-wf-powered-race.com/api/v4/cars/{carId}/start'}", shape=box, style=rounded]; +waitForStartup [label="waitForStartup\nlisten", shape=box, style=rounded]; +carId -> startup [style=dashed, xlabel="input"]; +start -> waitForStartup; +startup -> end; +waitForStartup -> startup; +} diff --git a/tests/visualization/fixtures/mock-service-extension.dot b/tests/visualization/fixtures/mock-service-extension.dot new file mode 100644 index 0000000..bc36225 --- /dev/null +++ b/tests/visualization/fixtures/mock-service-extension.dot @@ -0,0 +1,12 @@ +digraph "sample-workflow" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callHttp [label="callHttp\ncall: http\nGET {'uri': 'https://fake.com/sample'}", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callHttp -> end; +start -> callHttp; +} diff --git a/tests/visualization/fixtures/raise-inline.dot b/tests/visualization/fixtures/raise-inline.dot new file mode 100644 index 0000000..840551c --- /dev/null +++ b/tests/visualization/fixtures/raise-inline.dot @@ -0,0 +1,12 @@ +digraph "raise-not-implemented" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +notImplemented [label="notImplemented\nraise", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +notImplemented -> end; +start -> notImplemented; +} diff --git a/tests/visualization/fixtures/raise-reusable.dot b/tests/visualization/fixtures/raise-reusable.dot new file mode 100644 index 0000000..840551c --- /dev/null +++ b/tests/visualization/fixtures/raise-reusable.dot @@ -0,0 +1,12 @@ +digraph "raise-not-implemented" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +notImplemented [label="notImplemented\nraise", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +notImplemented -> end; +start -> notImplemented; +} diff --git a/tests/visualization/fixtures/run-container-cleanup-always.dot b/tests/visualization/fixtures/run-container-cleanup-always.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-container-cleanup-always.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-container-cleanup-eventually.dot b/tests/visualization/fixtures/run-container-cleanup-eventually.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-container-cleanup-eventually.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-container-stdin-and-arguments.dot b/tests/visualization/fixtures/run-container-stdin-and-arguments.dot new file mode 100644 index 0000000..602d803 --- /dev/null +++ b/tests/visualization/fixtures/run-container-stdin-and-arguments.dot @@ -0,0 +1,14 @@ +digraph "run-container-stdin-and-arguments" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +setInput [label="setInput\nset", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +setInput -> runContainer; +start -> setInput; +} diff --git a/tests/visualization/fixtures/run-container-with-name.dot b/tests/visualization/fixtures/run-container-with-name.dot new file mode 100644 index 0000000..3ad0d04 --- /dev/null +++ b/tests/visualization/fixtures/run-container-with-name.dot @@ -0,0 +1,12 @@ +digraph "run-container-with-name" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-container.dot b/tests/visualization/fixtures/run-container.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-container.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-return-all.dot b/tests/visualization/fixtures/run-return-all.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-return-all.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-return-code.dot b/tests/visualization/fixtures/run-return-code.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-return-code.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-return-none.dot b/tests/visualization/fixtures/run-return-none.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-return-none.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-return-stderr.dot b/tests/visualization/fixtures/run-return-stderr.dot new file mode 100644 index 0000000..671a7ea --- /dev/null +++ b/tests/visualization/fixtures/run-return-stderr.dot @@ -0,0 +1,12 @@ +digraph "run-container" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runContainer [label="runContainer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runContainer -> end; +start -> runContainer; +} diff --git a/tests/visualization/fixtures/run-script-with-stdin-and-arguments.dot b/tests/visualization/fixtures/run-script-with-stdin-and-arguments.dot new file mode 100644 index 0000000..5769f6e --- /dev/null +++ b/tests/visualization/fixtures/run-script-with-stdin-and-arguments.dot @@ -0,0 +1,12 @@ +digraph "run-script-with-stdin-and-arguments" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runScript [label="runScript\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runScript -> end; +start -> runScript; +} diff --git a/tests/visualization/fixtures/run-shell-stdin-and-arguments.dot b/tests/visualization/fixtures/run-shell-stdin-and-arguments.dot new file mode 100644 index 0000000..ecc5968 --- /dev/null +++ b/tests/visualization/fixtures/run-shell-stdin-and-arguments.dot @@ -0,0 +1,14 @@ +digraph "run-shell-with-stdin-and-arguments" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +runShell [label="runShell\nrun", shape=box, style=rounded]; +setInput [label="setInput\nset", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +runShell -> end; +setInput -> runShell; +start -> setInput; +} diff --git a/tests/visualization/fixtures/run-subflow.dot b/tests/visualization/fixtures/run-subflow.dot new file mode 100644 index 0000000..9b0cde9 --- /dev/null +++ b/tests/visualization/fixtures/run-subflow.dot @@ -0,0 +1,12 @@ +digraph "run-subflow" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +registerCustomer [label="registerCustomer\nrun", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +registerCustomer -> end; +start -> registerCustomer; +} diff --git a/tests/visualization/fixtures/schedule-cron.dot b/tests/visualization/fixtures/schedule-cron.dot new file mode 100644 index 0000000..7fe0b6b --- /dev/null +++ b/tests/visualization/fixtures/schedule-cron.dot @@ -0,0 +1,12 @@ +digraph "cron-schedule" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +backup [label="backup\ncall: http\nPOST https://example.com/api/v1/backup/start", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +backup -> end; +start -> backup; +} diff --git a/tests/visualization/fixtures/schedule-event-driven.dot b/tests/visualization/fixtures/schedule-event-driven.dot new file mode 100644 index 0000000..597abc0 --- /dev/null +++ b/tests/visualization/fixtures/schedule-event-driven.dot @@ -0,0 +1,12 @@ +digraph "event-driven-schedule" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +callNurse [label="callNurse\ncall: http\nPOST https://hospital.example.com/api/v1/notify", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +callNurse -> end; +start -> callNurse; +} diff --git a/tests/visualization/fixtures/set-expression.dot b/tests/visualization/fixtures/set-expression.dot new file mode 100644 index 0000000..59bcfa2 --- /dev/null +++ b/tests/visualization/fixtures/set-expression.dot @@ -0,0 +1,12 @@ +digraph set { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +initialize [label="initialize\nset", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +initialize -> end; +start -> initialize; +} diff --git a/tests/visualization/fixtures/set.dot b/tests/visualization/fixtures/set.dot new file mode 100644 index 0000000..59bcfa2 --- /dev/null +++ b/tests/visualization/fixtures/set.dot @@ -0,0 +1,12 @@ +digraph set { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +initialize [label="initialize\nset", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +initialize -> end; +start -> initialize; +} diff --git a/tests/visualization/fixtures/star-wars-homeworld.dot b/tests/visualization/fixtures/star-wars-homeworld.dot new file mode 100644 index 0000000..66820ea --- /dev/null +++ b/tests/visualization/fixtures/star-wars-homeworld.dot @@ -0,0 +1,16 @@ +digraph "star-wars-homeplanet" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +getStarWarsCharacter [label="getStarWarsCharacter\ncall: http\nGET https://swapi.dev/api/people/{id}", shape=box, style=rounded]; +getStarWarsHomeworld [label="getStarWarsHomeworld\ncall: http\nGET ${ $context.homeworld }", shape=box, style=rounded]; +id [label="Input: id", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +getStarWarsCharacter -> getStarWarsHomeworld; +getStarWarsHomeworld -> end; +id -> getStarWarsCharacter [style=dashed, xlabel="input"]; +start -> getStarWarsCharacter; +} diff --git a/tests/visualization/fixtures/switch-then-string.dot b/tests/visualization/fixtures/switch-then-string.dot new file mode 100644 index 0000000..7ea0235 --- /dev/null +++ b/tests/visualization/fixtures/switch-then-string.dot @@ -0,0 +1,28 @@ +digraph "sample-workflow" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +handleUnknownOrderType [label="handleUnknownOrderType\nset", shape=box, style=rounded]; +processElectronicOrder [label="processElectronicOrder\nset", shape=box, style=rounded]; +processOrder [label="processOrder\nswitch", shape=triangle, style=filled, fillcolor=lightyellow]; +processPhysicalOrder [label="processPhysicalOrder\nset", shape=box, style=rounded]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +default -> processElectronicOrder; +handleUnknownOrderType -> end; +processElectronicOrder -> processPhysicalOrder; +processOrder -> case1 [label="case1"]; +processOrder -> case2 [label="case2"]; +processOrder -> default [label="default"]; +processPhysicalOrder -> handleUnknownOrderType; +start -> processOrder; +subgraph cluster_cluster_1 { +style=dashed; +labelloc=b; +case1 [label="case1\n.orderType == \"electronic\"", shape=box, style="rounded,filled", fillcolor=lightyellow]; +case2 [label="case2\n.orderType == \"physical\"", shape=box, style="rounded,filled", fillcolor=lightyellow]; +default [label="default\ndefault", shape=box, style="rounded,filled", fillcolor=lightyellow]; +} +} diff --git a/tests/visualization/fixtures/try-catch-retry-inline.dot b/tests/visualization/fixtures/try-catch-retry-inline.dot new file mode 100644 index 0000000..505476d --- /dev/null +++ b/tests/visualization/fixtures/try-catch-retry-inline.dot @@ -0,0 +1,21 @@ +digraph "try-catch-retry" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +tryGetPet [label="tryGetPet\ntry-catch", shape=octagon, style=filled, fillcolor=lightpink]; +start -> tryGetPet; +tryGetPet -> end; +tryGetPet -> getPet [label="try"]; +subgraph cluster_cluster_1 { +labelloc=b; +subgraph cluster_cluster_2 { +label="try"; +labelloc=b; +getPet [label="getPet", shape=box, style=rounded]; +} +} +} diff --git a/tests/visualization/fixtures/try-catch-retry-reusable.dot b/tests/visualization/fixtures/try-catch-retry-reusable.dot new file mode 100644 index 0000000..505476d --- /dev/null +++ b/tests/visualization/fixtures/try-catch-retry-reusable.dot @@ -0,0 +1,21 @@ +digraph "try-catch-retry" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +tryGetPet [label="tryGetPet\ntry-catch", shape=octagon, style=filled, fillcolor=lightpink]; +start -> tryGetPet; +tryGetPet -> end; +tryGetPet -> getPet [label="try"]; +subgraph cluster_cluster_1 { +labelloc=b; +subgraph cluster_cluster_2 { +label="try"; +labelloc=b; +getPet [label="getPet", shape=box, style=rounded]; +} +} +} diff --git a/tests/visualization/fixtures/try-catch-then.dot b/tests/visualization/fixtures/try-catch-then.dot new file mode 100644 index 0000000..247c417 --- /dev/null +++ b/tests/visualization/fixtures/try-catch-then.dot @@ -0,0 +1,34 @@ +digraph "try-catch" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +buyPet [label="buyPet\ncall: http\nPUT https://petstore.swagger.io/v2/pet/{petId}", shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +petId [label="Input: petId", shape=box, style=filled, fillcolor=lightyellow]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +tryGetPet [label="tryGetPet\ntry-catch", shape=octagon, style=filled, fillcolor=lightpink]; +buyPet -> end; +getPet -> end; +petId -> buyPet [style=dashed, xlabel="input"]; +setError -> buyPet; +start -> tryGetPet; +tryGetPet -> getPet [label="try"]; +tryGetPet -> notifySupport [label="catch", style=dashed, color=red]; +subgraph cluster_cluster_1 { +labelloc=b; +subgraph cluster_cluster_2 { +label="try"; +labelloc=b; +getPet [label="getPet", shape=box, style=rounded]; +} +subgraph cluster_cluster_3 { +label="catch"; +labelloc=b; +notifySupport [label="notifySupport", shape=box, style=rounded]; +setError [label="setError", shape=box, style=rounded]; +notifySupport -> setError; +} +} +} diff --git a/tests/visualization/fixtures/try-catch.dot b/tests/visualization/fixtures/try-catch.dot new file mode 100644 index 0000000..cf311fa --- /dev/null +++ b/tests/visualization/fixtures/try-catch.dot @@ -0,0 +1,21 @@ +digraph "try-catch" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +tryGetPet [label="tryGetPet\ntry-catch", shape=octagon, style=filled, fillcolor=lightpink]; +start -> tryGetPet; +tryGetPet -> end; +tryGetPet -> getPet [label="try"]; +subgraph cluster_cluster_1 { +labelloc=b; +subgraph cluster_cluster_2 { +label="try"; +labelloc=b; +getPet [label="getPet", shape=box, style=rounded]; +} +} +} diff --git a/tests/visualization/fixtures/wait-duration-inline.dot b/tests/visualization/fixtures/wait-duration-inline.dot new file mode 100644 index 0000000..0c490bf --- /dev/null +++ b/tests/visualization/fixtures/wait-duration-inline.dot @@ -0,0 +1,12 @@ +digraph "wait-duration-inline" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +wait30Seconds [label="wait30Seconds\nwait", shape=box, style=rounded]; +start -> wait30Seconds; +wait30Seconds -> end; +} diff --git a/tests/visualization/fixtures/wait-duration-iso8601.dot b/tests/visualization/fixtures/wait-duration-iso8601.dot new file mode 100644 index 0000000..467435a --- /dev/null +++ b/tests/visualization/fixtures/wait-duration-iso8601.dot @@ -0,0 +1,12 @@ +digraph "wait-duration-8601" { +labelloc=top; +fontsize=20; +rankdir=TB; +splines=ortho; +node [shape=box, style=rounded]; +end [label="end", shape=doublecircle, style=filled, fillcolor=lightcoral]; +start [label="start", shape=circle, style=filled, fillcolor=lightgreen]; +wait30Seconds [label="wait30Seconds\nwait", shape=box, style=rounded]; +start -> wait30Seconds; +wait30Seconds -> end; +} diff --git a/tests/visualization/test_graphviz.py b/tests/visualization/test_graphviz.py new file mode 100644 index 0000000..0010c67 --- /dev/null +++ b/tests/visualization/test_graphviz.py @@ -0,0 +1,58 @@ +"""Docstring for tests.visualization.test_graphviz. + +Tests for graphviz visualization of Serverless Workflow spec examples. +""" +import subprocess +from pathlib import Path + +import pytest + +from serverlessworkflow.sdk.workflow import Workflow + +SPEC_EXAMPLES_DIR = ( + Path(__file__).parent.parent.parent / "submodules" / "specification" / "examples" +) + + +@pytest.mark.spec_example +@pytest.mark.parametrize( + "example_file", list(SPEC_EXAMPLES_DIR.glob("*.yaml")), ids=lambda f: f.name +) +def test_graphviz_examples(example_file): + """Test that SDK can render graphviz visualizations for spec examples.""" + print(f"Testing workflow from {example_file.name}:") + + with open(example_file, encoding="utf-8") as f: + workflow = Workflow.from_yaml(f.read()) + + Path("tests/visualization/outputs").mkdir(parents=True, exist_ok=True) + + example_name = example_file.stem + + # rendered_graph = workflow.render_graph() + rendered_graph = workflow.render_graph( + filename=f"tests/visualization/outputs/{example_name}.dot" + ) + rendered_graph = workflow.render_graph( + filename=f"tests/visualization/outputs/{example_name}.png" + ) + + # Render PNG visualizations for comparison + fixture_dot = Path(f"tests/visualization/fixtures/{example_name}.dot") + + if fixture_dot.exists(): + fixture_png = fixture_dot.with_suffix(".png") + try: + subprocess.run( + ["dot", "-Tpng", str(fixture_dot), "-o", str(fixture_png)], + check=True, + capture_output=True, + ) + print(f" Generated: {fixture_png}") + except (subprocess.CalledProcessError, FileNotFoundError) as e: + print(f" Warning: Could not render {fixture_png}: {e}") + + with open(f"tests/visualization/fixtures/{example_name}.dot", encoding="utf-8") as f: + expected_graph = f.read() + + assert rendered_graph == expected_graph diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a3e2ffb --- /dev/null +++ b/uv.lock @@ -0,0 +1,1341 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/72/f704a97aac430aeb704fa16435dfa24fbeaf087d46724d0965eb1f756a2c/bandit-1.9.2.tar.gz", hash = "sha256:32410415cd93bf9c8b91972159d5cf1e7f063a9146d70345641cd3877de348ce", size = 4241659, upload-time = "2025-11-23T21:36:18.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/1a/5b0320642cca53a473e79c7d273071b5a9a8578f9e370b74da5daa2768d7/bandit-1.9.2-py3-none-any.whl", hash = "sha256:bda8d68610fc33a6e10b7a8f1d61d92c8f6c004051d5e946406be1fb1b16a868", size = 134377, upload-time = "2025-11-23T21:36:17.39Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/84/2cfb1f3b9b60bab52e16a220c931223fc8e963d0d7bb9132bef012aafc3f/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709, upload-time = "2026-01-01T23:50:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/3127b277e9d3784a8040a54e8396d9ae5c64d6684dc6db4b4089b0eedcfb/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658, upload-time = "2026-01-01T23:50:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/b91b093a5c42eb218120445f3fef82e0b977fa2225f4d6fc133d25cdf86a/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026, upload-time = "2026-01-01T23:50:50.853Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cb/1ded77d5976a79d7057af4a010d577ce4f473ff280984e68f4974a3281e5/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529, upload-time = "2026-01-01T23:50:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/6ca5bdaa701e15f05000ac1a4c5d1475c422d3484bd3d1ca9e8c2f5be167/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271, upload-time = "2026-01-01T23:50:55.287Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2d/55c0e38073997b4bbb5ddff25b6d1bbba8c2f76f50afe5bb9c844b702f34/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039, upload-time = "2026-01-01T23:50:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/33/4e/3662a41ae8bb81b226f3968426293517b271d34d4e9fd4b59fc511f1ae40/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505, upload-time = "2026-01-01T23:50:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5d/cf768deb8bdcbac5f8c21fcb32dd483d038d88c529fd351bbe50590b945d/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570, upload-time = "2026-01-01T23:50:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/ee70effd13f1d651976d83a2812391f6203971740705e3c0900db75d4bce/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600, upload-time = "2026-01-01T23:51:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/f0/eb/dc098730f281cba76c279b71783f5de2edcba3b880c1ab84a093ef826062/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977, upload-time = "2026-01-01T23:51:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "myst-parser" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydot" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +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/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "roman-numerals-py" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "serverlessworkflow-sdk" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "transitions" }, +] + +[package.optional-dependencies] +all = [ + { name = "bandit", extra = ["toml"] }, + { name = "mypy" }, + { name = "myst-parser" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +dev = [ + { name = "bandit", extra = ["toml"] }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +docs = [ + { name = "myst-parser" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, +] +viz = [ + { name = "pydot" }, +] + +[package.metadata] +requires-dist = [ + { name = "bandit", extras = ["toml"], marker = "extra == 'dev'", specifier = ">=1.7" }, + { name = "jsonschema", specifier = ">=4.4,<5.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2" }, + { name = "pydot", marker = "extra == 'viz'", specifier = ">=4.0.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3" }, + { name = "pyyaml", specifier = ">=6,<7.0" }, + { name = "requests", specifier = ">=2.25" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1" }, + { name = "serverlessworkflow-sdk", extras = ["dev", "docs"], marker = "extra == 'all'" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=2" }, + { name = "transitions", specifier = ">=0.9.2,<1.0" }, + { name = "types-pyyaml", marker = "extra == 'dev'" }, + { name = "types-requests", marker = "extra == 'dev'" }, +] +provides-extras = ["all", "dev", "docs", "viz"] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561, upload-time = "2024-11-13T11:06:02.094Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "stevedore" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "transitions" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/83/9e90f3494057d73c9e6bd8de8488af7c9fa714666ccc2db30fe07d147818/transitions-0.9.3.tar.gz", hash = "sha256:881fb75bb1654ed55d86060bb067f2c716f8e155f57bb73fd444e53713aafec8", size = 1191029, upload-time = "2025-07-02T10:49:03.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/05/fea3020e90aa378941ff10de8860fb3b1288313c27efd10fa0c476498f7e/transitions-0.9.3-py2.py3-none-any.whl", hash = "sha256:02463248f2b668d86f66636b1e3c9e8de84d93e22915247f4e1aa9ee1cae28aa", size = 112792, upload-time = "2025-07-02T10:49:01.415Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] From 3e4d1ff3da0c426ff73e4e44e72d1f7d23d45aed Mon Sep 17 00:00:00 2001 From: Armin Graf Date: Sun, 18 Jan 2026 23:14:47 -0500 Subject: [PATCH 2/3] fix: fix rendering documentation Signed-off-by: Armin Graf --- README.md | 10 +++++----- .../visualization/accumulate-room-readings.png | Bin 0 -> 32648 bytes tests/visualization/test_graphviz.py | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 tests/visualization/accumulate-room-readings.png diff --git a/README.md b/README.md index fe19d2e..76089d7 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ print(yaml_output) You can see full examples in the [tests/specification](tests/specification) directory -## Generate workflow state machine and graph +## Generate workflow rendered graph **Note** Please note that `pip install serverlessworkflow[viz]` needs to be installed in order for this to work. The `viz` feature installs pydot, which supports `graphviz`. @@ -150,13 +150,13 @@ To dot files: workflow.render_graph(filename="/tmp/out.dot") ``` -The following requires `graphviz` to be installed (verify by checking to see if the `dot` binary is on the PATH): - -To png files: +An example rendered png from the dot file: ```python -workflow.render_graph(filename="/tmp/out.png") +workflow.render_graph(filename="/tmp/out.png", format="png") ``` +![Workflow Graph](tests/visualization/accumulate-room-readings.png) + # Local development ## Install dependencies and run tests diff --git a/tests/visualization/accumulate-room-readings.png b/tests/visualization/accumulate-room-readings.png new file mode 100644 index 0000000000000000000000000000000000000000..6471f346e61d6c8585e8e76b4e867482af7a404d GIT binary patch literal 32648 zcmd43^;=bI*fqKcX{3=xKqRF*m2RX2lm?MbNhzfpq!nopPzeJwFG9exf9twpbR8>*XMWN9A;cE;B3;x9~ zNbU-J!?IFWRzO`M|K&BlPC=orqf`~{==)}`Py6VTk56H5Z}D(<6muBdaM(@39E1w4IOLVYD4l47yb3p)`Y02D>`qltEBX!ADy=|o;D0y`b3)M z42b8D**@9~@WCZ&eyEypx_;wBs9@+Z*ck|i8+&G1WDGV#k zI{x%#kT8%WydKC|m|pW?cg)5~>D|<+jb*_ zObRnIjZd(HgM;Y^q@qrbHVZ>2^2}oW`JAos3YC=j9BeQ#)GlbicYp6zY>UObp}=S*ey28Qt089B^n3S#ev zKMcO{;nm0yqYlL)L{IEW8J5%$(_g`5dY_h&5s86)b*^?cxcvv4_RxDD^~aC5qSCM4 z$}i`6Ib*GpfRlVDxo$BSJCR39ZjbUi|7FIBo0nI3OiYYMzj3?{>{)j|Y!<&?kBwBK ze}cFCROrSZKKNomRB#uioqY%E|$+8}@~A+Bjuje*d$71F0Uw3-d7o~|g#Jb|f#>Q?mD`7{Bvx{AVvYbxI z*fNQIJ2D&bwL7l!LW8*Qa}(v~oDd;Nc)h*7p|Nl98I)wchP9L3{cPq8w_^MX%Zrc6 z4V^(js>SVSUd<~s9df0pjq=w0u)S*o)+Q}tRHFP#5(y)jLV~Rm=hp6r>&C~&$8Pk> z4yA)+a-6@1i`9?!24wkv4>0k$y7qg~m3>&ZiEdTLw*EU@EatOGH|}2a4B3mGTOuNq zW6p0aN89mOMB?Wp5;Qibo3Se^n>*XFi7cJo4hGi@em9a+Inm7Jrw?FaY`;gaC=t_F z|MoGO-lNQ&J%WlteQFASIhA^oIyW>lY;?Zkqt=C!`LwKtbP23W8LErZGxxv4tQCv; zxL(u5ay>UlqX!EN@Nr(^@Y^0yRV;RoiX1t22d2eFZR3=dbN(&}81a(F;h2I9CD{|e z3O&Per&rv>Iv2qiD9mY}oy}ciPg634z0>`(yj*A;Ys#?FX4u`3Q#dR}<=8$vkLlfnBjY{f9;sb6xJ~fHFIB@FpBJZzRnMQA$lv|(voUz7Eq4eKHLRwHC8Gg}lqEl29 zx@jrqJrfC?nrq3&Fi|e!L!!Zo^~R0syOuXzOo=W_sMQR8RMu0FifZXfWUNonWkRYz zM?=7gz%gflkyrHs@`}0#)*;xK*w~)*#d65W$57DG#bJpM=HkqVw!sPUpQ!ECelApqrM&#g*r=kG z|JC7zA4zOIT%T^)500FiWswId#e^Vhiu(9b2o>@o{e}M2`TjiFh5H4zQhYp;XHt#3 z)v1Bd3^BLxfpsv{`=Z(Jwvj|h)E;MW_0*gwPNwKbWKMkUNiiN!xd)@ zCpqm!xHC&_{`kIG_xHYz@6)1|BVTb2-mXzhr3;lh2Ae%D-WJrwL{3W)8koGI!4-6` zr}|UUmq&gh<$CyqpW@L`HpRB{!wu?uPIfp`-#*pVAKjb`nx&*Sk%&qh5*_c#e3g4m z^2FuLCv)75!DYP(t;;yU?(YOCir7;Ut1);pUaMVlesau_^%E;0Iw6|Az5#OhFdJ=sakS=D6EuK7wa;Njuf_+Pazcv&5m#0=dxiMK~N$fZetYGZB^O|>#*ZhVGF zd6}`}bL3W!lu{ngZ3h$185!T$Yog{M{1L-r>bh@Or#DL~qdgS5$^P6?R#q-qPQKYt zz!pZH|L(kAR#H-$woO8=tY+$0lX%IitcbO#ip8;+2RJiS1YJK9RwQd(t53UR5<2?% zCG#JZM=H&VJc~$BDpNFL@EG<$on1aE!uc=O*}vTXesN=3jtE6bf6HQM2wj|7d_vTZ zvHzB2zX@Y;;XDnhBwn?}4Hu?*%dC0-4Mrn|KU|#^1a#EV{LKxTNbxw4<|A zYJC^`-@kvl_Sy{k0r02p>K&Z2paeQe^yCJW^1UI)RXgT?NsGFeDl}J@`jC%+cpVG^0p=58Jx2Q>|E99A}BYV#|qGuDlqR z?B<->R|<_2AG~p$@-(l&@z$A6hL145cbU`v!H;Zo3|yjkZ4X|B>aC0F0;0_cG1`3>#O(4 z$92Sx_Ivc;|5~P}U;WPu3OeIy3$2$E>ONdvg;)3-wU`F{IG`1>XKq}^`yGxH5d9Q3 zEv1QSz8lS&OA>gGo}01Fw?<=UO1TR_;s3v{U{Q(-Da+#EdDr+A3cra-AjwTH&AFwe zPOnt8e55+2-M-$kp7p`qao|?KzdW(tWmWZ_Q}SPj&GA?EC~sd++LYtomD5JLaq6pA z@tvBqg`1*Y{8m43knWh{{$zXSLRl*CjC^%<)ugM8z+WJpSXYFQknpW>4VKTsn2ACVSVS==)Pj4IJKMG2`=hui$H|MOhh*M$s(C2i4?A1T z``Mb9!5yTP?mnpXrXRNY;TF*9UQyWntJ*w)iA4ruZS3u$gjyPc98+J0hhO>RI-P(d ztJ$11+*#kb^%2Q?E=*dTnA9Gh6=RnNz4Gdetbbyt z9#PxW6s>#&!MsF*{#_(G+B`fw=H^yHB6W!713PKGR>$Whp6f&HYMX3w94hgn#e9YP z?8OV3@#LbX|By?+uBb>L>(b84v6OaV_(c7e@du#1hktSZPDnWk(qsy?>=bsT6?bFg zRnW?*u>M_LWnR%B7Qz>*p0SsHK6AsJzHAr{(DKy}i4nP5+0ucO5)u;s1I5(dJgDW7 ztB7o=N@p4qt7!ck4wRF67l*^F9rQ&2a55a__WMpKR=2s)U^c>&% z+)vz2nKx19tgTwKG+L~~8q<8|zSvC5Cn&fm@mvE5RsF1-9ESc^(%9NQ+D?o$^vX*N zcg(aH#;es*Gg2a3jGs>Yslm6F>bkz7ku>Zz9uTbrSgDBf`gKU$xKJ}`X#!&zOlVht zGUXnt2wfeJiu*VrLhCuU-OoJMu|xCNbJM`I_w_I7IlBxQYxY(;+fBo0Z?+QZg5$ueiIruZVqNp|cggJ)o~$ zK|M@5?Dnvo!erKi-pgUUl~MgNTcY5Tpk&V4bjtc)YY^PV*lE4#@lDWRsH zy)6NN`MO=A5ztb#*=7>HIJCoyy-Io4!0?^!sh_xCR6SWJTSm@Y&|Kh+Ce1L-C$t(_ zLRWHfa?}n``d9uVHs%TnnBH46@u87K3!|}ycPopp_G+c`Ia*`=52542D0M88*VKD| zd)f96Gn+Iv?htNNFhgS4GOq8@9KG4Epr^mynivUv3|ZqdjH44Lz4hSJ?-lxcT*!_7 ziiwVHEmqHP`_n@aCQZ=3_h=wFEsHhiRoC*JQWi{-h4yfw~cX{0kwum-n0p z3zdoXp|~wAEqMeJjnW}7>Y$CP{iUeu_U<3`qT*sEX=!>%pG{0u9D_8?{rmR^$HpRx zi&QJbw3{u_hq~QvSa4QNwkp&a8Dg&gHOGPe=qvL_xqr$iU zKmXu$j~7Y~=S27JU1A>dChqEu`SwUuNWJH>n$y-$AvPU7{hxGAogFIcxHaN1V%?S9 zKfNnIKd@ndkpL1}VuNFf3aRtaQGB7aQ)zjm0XA~9|_VxGIP3C1oT98B6_wR)D z_4TYxIWJ%0i;9Yh-_&(NHgr@g@+!TBy}j|m8XThCY*Qm08HdP!Z~6A)2O0X)yl!u} z6rA4ui>KsN$N|^Z_ zrNuRhGRY6WO@KI(`Qk-eZ!dm4ttgMzEHFch)@N5(o3y%q?7WxC(a-t(S<-eO=hlS; zFK=RlGv^&k#!L@yZ!8p@#N(*N8)3x8t7GqIU4GS}p;m^A@hi-m``wwt<%u*%&GCSXn?W_V|NJBi9xl~c(QzY%I7@`|KauS}QX zVuyDZI`DrrKDh?`Ls3l)Z}`WLKOSZkl>7VpgCFhn9B z*WS<)Uz$l+Q%(;3lgo6>^k<*NZz*hd4Ga{a9_E8y6Hd^v{i)JNTaK4ztf!kTdRckcXTJSRJ$)KJ^nj<<-dN*B9H_{ zidNKRZlc=Ss3}1F;&_pW`~F89z1Lc|yf1R!v#*Q!`*jZtC~IgCJb(T?>BWm4FFIY- z7cX86{`e7EUoQ?A+QvI46q~4Rbd+NFEE#YOrBWh8Ja9_OvA4#Vf{(5`jFz+39I75< zWMxImUY?Qlrr$!|>Am?Yy8o4=VG*u5A2XAPNc#D~xcS>BN1Gb*@}X#X!D_pS1Ir&^ zRn@sc|Dvax0w1hiZee0#^4<2A)jd7hdPGA*^A>iM`{qq0hYivWax${^fBS#ig@lAA zF3wN6JW7eYrq8rt6PE^Z6JJUC$US&KtEHv2y}K*-=H9!!%1R3%#Xo%b(49WE zd*Wbi+;|{InmVz|8VWr(4^Kspv)Wor(9|dAxUR)&__1OAt%B^aO7j8WEby-n{Jf8J zCgfy;&U{6J&W<^2c7SXy{r>%RWOVfB*~y-KE^i#jjsjj@UY{V3IBVonXW=bbx361V<2B=UG-M2Kq<%1}`rWKKC>{+1Ypy+# zNDMwm(ba{R70w*}+O=z8VPO_qGmTVIzI<%rx_Wv;MXF>IlatKXuZJ)YGZ$OMwZiGQ zwzm3$R%1WiE9|NJ;pIG*`PR_xhp*wKLAcyk&$lY35v2&XIh-+zSx$>)C@a*gih4NhPR|N4c z{nmqjG7L`3YyE9fEKjeqgPNG1eLR9QN~7lp(U#DCntvLJ}H)Wys_$j-m@ z-k5aS{8jHEZVn;&V`RkU@@y~Br2jQHL|HqEPgYhoJt?ULR?-Enrq9XQnL2mi5gHnr zzyMN0MC=By?~LEPd9$yj<@VC@`6QI)%#xCur=WMZ^PY}ms$+Ljm1yTCRG5CYI^Lc) z>0%nqOim^V2n^ggnhjq3`Qed(?CiL1nnY%#R8wfzp7ftN9hie7_5;ypZ|(OXHRmyAeCB7XazHo@g_vVdle6g3ob z%Gv8_#g`YSM6ew~;|-})G&F6{7hGq)B%jnbZQ2S73x{WAW$91a#xe2nRhj&#q@to~ z>FmV1JU=q8uH;%zsit_97zE{QWvZ5U@}oUVp@KqIX?eNTQeRf4m^(8mDQSBGh={l3(d>Be&gm%Idye>=ygV4eEA*5E8_e5G@O3EzXDO)-Q67m z*yPKXFN0-zY|!PbX1@5zeTX&}V<)=fz|N8YSbKw5Nn%alGlqQY-6d+>}%=jv4@ z=+}LHecL;W%0l+Tn5Zh3X$j0LSDp?mFG6}y{L2;;5U7H-fV8QPr zX;1?jNkUC+d=|UAp}L@CWyP$gqhkr!VL<{<=Q?DChMr!E^K?Uv$*CdqZirrkm7v?O z@1Bnz+r-ldTZy{P*uc4ROG#zdxJ_& zSq$dnC^QRbd5@q*TR}H?2ubk>H36AIZJm4CqN%C*6ZX?2_2R5EB@bnPb7~b)wv|IbP?tiq1y~fLPbODIl1~mam%=(Gf_$h%cT7AyX&;QD43^f8K zVO7=DB(9DVRZPI_iRtNYxvyvPTN6`JMbS%oUuR({oERvP+Mu}l6H7)muBfCW;_X{* zR12I%KKvbW{odbm3`N=Hx;imbl95!2n~|%nt!>%>HoF^RpyHP&M+ppZ%xP8tDSWnO z-09`zoSe8JR@AeH1OOjQbj{BFNZ!bZijv0!$K2dpOk8|^tN9WL z*2B@6+u}EF+z9DfY@L}g`|{2$tFto0k( zSE1g{(Q)y|Yi*ORq`dt6r@wyv(lzHbu|7}_?*jOdx+XO_IVtA3M35sJd=+XN_k&La zhlhtA)E?y(6<1A7r|~LDBwc5u)+eelAb=xYzI=In)8BXll4?Ft_To11DX2o|GP%4u zf=xVU!y}MY$TgapntYEo>BHohkm?RRtL@u2RnFy75>wGQe*eiNeLNU0uEyvL4>ou? z!ciDfvjdVw5W^_Q3pGPKNud-n{j#qSS&$nAXdK5WcX`WHHp*}P;WYqAO z!^Fg7Zf-7wNj}2j;_L+ak4v!t(u{@*6sd&ll13`NLN*yZa2z_Hbj0Z6-Z6H4Mr|JHVz~b6coIP9vo{P%l60ir>`A?tbj$^=99U%6c6#`R%=T_nlx0xDJSW z9oob?6z}!VTTRyL=}in4%m3pA;4bqx{i7qsjqr32aYkzjT2;NDCo5bMs}cO2TG?W) zP$e_(vfDsn+kl=6NXBJ>mGo$9b|5Z3{wI|CpZkA*WTmB1& z&jJ-26=i|^LtmC40{*==e`QJgZim!={w(O}=_vvT0{ODFxBvFZ=_hmlT`}(Ak&)D& z6BAYTKT7!N>FLKl`?yjvF!XEXNPR<9H3go&pQ?5J0G-7f0JN=@RomnzXRaLOa~Ooa z1zOQJIGFTGHrVXf1?+DY&`Ci-L2IO~K)PB2zlh`S+tN{b} z{rZ*t^XJcMh*Umxb#>^cl1Q)fo^QJ{*7(Gyq`0^vfkC=wVPT=?Ks*vk2}5ERl(L^b zvw>XzSUWacf7K;ZkdvpbT}BTpO^-yWeuvmc1($~l9IH^$;sfb2!W4Y9Vx>=)p;%_d=0Zj4VD{lL3fI3ZgO9Ys55T_x-mF1 z!d)#rw`UOW_^}WG4NEJlXV^px*Tu!Dp^n@Ae9sy(pqDNC3X93g`UtpdEADP&LD1GyavO?`f-P}#is_X8G} z&U=QiK~RZdX3+|%-w`||Pos$fld zd5n$88m{+^aJ0oa*z7@H)l@goa+k#ffSv`L{?glr(m=YAexTs#UU=SgLC~Kg9SbZ7 z=$zUwc3#*^(82T@l*$G(766G)GzZH9%*OzcD>Ez05~9g&>eDSAvoBF0A*in%9iR~9 z!-6tp0wqAeFxWhV%!!5O4QN08e$tb}^=SC%JZJ?A(Xo@Yu8CmlumF6AoHCTSD|371 zVqnfu1i%cT198MObnng`H1G9^r_e81WMvrvYjy!yxK55&ZPgJ&Iwc}1+Es1cg@AKp z!m#iKM3z$X=Ahy!A_4*hfHvFz4je}+OlcDm5*FTEgSL<0btvPVkXMl)J}g3~1{o*v zD{=F1lXmeRrXmS3-|g!(LUwm;g!FxV#ULREE6tnBbBjz~$&TJtEa9loU_5`$s*cn; z$RNlY`*kSgPG6%)7G!!j^h&A_^!VrZ@2bp9s*=LO_AiZ%9bFl(6Pvcz-ZKQt`tP%2 zTyJ9A*{GcrfI_e|H`f9XAQPr3*6W_RWpLG}PoHuN3#T;(U*>>(ueLw*#Lur9c=>xk zs#QdcGJKMfl4JSu*zU3xs?OJBNE0rlqgNIe+W|8h+;Dt28GNux0fd9&@bo7HzB{A+ zd`gn};ZVjwASai$aZ$~jk3HuPW$jdDSf1lh!4-scAjnzpQNIlsifj;Qe0sRSm31z@xtUqcu`t+wY^|+XE*hl5gHCq;{PFKQfiNKm zIhGDM-Kc#FiMB9~fsSruV{-%Ju8q!LUe`{UptDRg3)uQ=%|TF_qr3jbI)1(js0Dib ztGc;2rKMm{aoveuDE}yH(@k7bUfwg)6v!2H>&A^Rm<{Tk9gK%Jz1`CD+f9XOn+~~f z5MEe}1mGo*g6CFN*sDQ(+@zBB7edXQZZ&7r?G)$d>-@IWFb3HNEpH<7L6g3(d|45; zDU?rQa&k=63dC0Q26!egLKGMu0-Qn1`)a&}=W2ri>&1Zdiv;1U9VS&!kBmNh-?Ht` zHvV(?TJvSZmoHLIGhd9)CYQdy=hrIppF=)4?-WPoI2FAr2v|71s|1Cxe z2QCvT>^NotB@MByA(cY*;Wpk2n!+ut2+aQ1^pps*Nu+<4n4HvQ)Kycb|k(FwL_XZvXMVf$NQGM{GOvpe- z)s66A)6~OE?|E}#N$>U2?XzWwJZ=w-LZx^<%Qnp8fB%^I`K#47FhXi;MS%*%vM489 z=itDFZ(-#Q-!5a*4+6^_a$?HY1NOtZU$l8ogC$CpTlc!=y9c#cA*8u#(q60N#{n{k zUQk+AX7Eo+NlA$cNT+xw}>m!sQv&*PgS};TaCstL-f& z%2Ixo=HkMKQ{fX4sn9(|&24U8#lXOLYu?Q8*?WW2z1u$jeUrAqcRd|wy!#pGW$EOf z277gLWdbc8J`9ImPIP8TCZj?#5z<^q*Fni?8m&CRk( z@ibSwybd^Phyt4Aa``OLfePN-3DH*V`6pu_OjOo@)W4PxG+1@i*F%8hR?xlaip%{< z2d_a-oCCl#I6fX3A5RE^f~Bi#A|gORW>SfJ+<*c()D-ySqw6dK#M3odTwjpJ0nI6& zVa{am?^8g+M2bW+XlV8ia@{VPTlr zGJ*G{Dr*6WBP0QOkkS47ZDV7UP(FRnj`@MnBhE6Ma5+G?RMpkhP-|t@*VhLD!H7vn zEFh8ni4Zj#ctj7v0U95I25fNvpwb7+zMVt5cIIOPo8|R`9`|GHcsT?;xf?` z2nDnI`}YpOwg`biDgg|$sAPkr{vB;4QgGcv99H%S_RMPtq zx;;T>JIZ0<;i`RMV81kjD`12EtN_z>x0OMrZ0RSIAmO%yD&Yn~L0^tEEqvfYjmc-P zHLSqE!1bwGA`rOT+}v(M0Z!*NPk_RKf<~$4@{lfC(sF3~~_a87>5H9F1^HyY}r1 z^23;*wlG{|@Kk>YNconPm6ct9U|AL5>1&d)mhM=DG&hEbG<&w7k0T)Ng|K79{Tc^L zAQ+(NW9JTBBeJB#!5kbMMCA^2LC6x5kUWJ|w)FK8jE#+LgNgvn?8b%v7R;GIQ;HyA zQL;GNoJQc^56p3Lyay{ODJiV?nmOX%y}JdJ43WU8Uo-dIfbzn`#H3ha#I8EOhe=Uf zTpSLh1CQ~w#@n}V-8O%T9|H#l27{83y=(Lw(Bbw;0K|P%t+3MsF_aTl8Br!;954?X z+C(cpOwP$MTljGTAIBxfPHqKxh#b|pP82%m4@58#8Eg&?&dTZP?k?>lI?5JxWuT*@ zt8$x{8+rHO8Ytq8Ukm8X)m2nb;4W@#l!i30fYSw;9Idqe$Ahn(o#|rqD)B3Y*dYK4 z5Yus;?+!EJ5c9*H?(UWt3N9eJ#`EHFoaOKqG53WqjRBo$dyod;kbN)C#Lv#ptDSxd zz&rpEm^(Hr|6~8ACiNc06XSckyEd7h{PFPdxjaHY-;J*;z?P|86>yuot?^2trtc>+ z?gkBxn8g%+>yejjS9%=u?8p(YC48lQ-HCRs0L8Yr0i^y^@SDT zA!Hm4(A3Is8{Oe~0$vP#~z-V5*5ugFoQdjjt2gysV1lH|(%Bc#q-2`#9n9K~XrISjt;% zP`uoC78ENMlN{IqhC_@$tEusdCaYI{84-a6wKN1hM} zM@&l_3zdzTlM@e)Sr{Ts6lhW0>wKNE`SWQz^a7|7eFJ!1SoZO zIOU2=B7l;`f`(cMOm3l!7x}zr&(L>TVu8j<`S0TZ+Yx(m$PK`mn2aoRd)qBRmiq6v z{{Bv|49)-j%aK!k;D{%A<=KZ1f*}KrMfWhR3BU)0Xjvqrq|czEe`99Qxef3i6u1tk zbZ%>7w;+-XU6vDKW95OV@q;GVmn&=TvdkJYFfc#_Fp>?%Yj}EkKNh@q3?_;(AuWtS zZYoar-qT}+5LMtN>!1khYAKQHz);iE&CLR6!a7Xy(i{+(x({M^m^z)uv+b4!U-ebRXu~}ma}Mwsah2$n8xhvuCue_09hcDy{YL?0aYds0GTgL1)hb5vUb`U zDi|ym=E6Qg#(|aT-YU947%F$X+|AQ7G%JhRwl7m>wFz?RjT&q)n^txU41(uZS7Tr} zmMYAzQ>})8ue80C5Xo*oLscxVX5M#htnYmGU{biKv*EHh?c~7iWG^3d{tIGIVg=Vx9&wlC)Qe zO=2)nLVzbyCR}G_b{3Y3Y}K{3yP`-~km3Yd3t2@)#UF7Ech}AyHJC3*2Azo^vXkRz zIRzOGCIRYP?^&?FzkgqUe*y^e$sJu?>gSjA_NGJE@NjW00Uy1ETColj5?#j(_7cEx z??Joahj9ueDXDIj&mY$I$HPC0)vtj-MT;>aXAmKI0i*{bGFhg+`Q|KO|G%AnymFU-UVRd~*b1_1s@uyIgXPk?w8j@Kx_kxi>K*8GKY z5wuRD54MDziHzF#Zw$%y*V5Tjp*F!fn0R@KUTfznLAsA8Bn?igSkK!#X&c&@^HmvpMPLHrsSnci8xc?3M2uL?Ev4x%Wb}Ed~yv`R_IpglUPi%qUD%|=2*Z0sO(V+M<&DrQGVcGr zf_|r-GO@aGvg8u$~MHhXL z)CeIa>%2E;l*@2*xRD1BnA8%$oAkd=IKju$u-v?f1p1r|mCYADUYK%Xh*c9Fl|uYPQO^Xd^|D4M(gnHM;Pw)J1tczIYXEg(rS!7xd9tzv?evFzGEQ1|Z`8iJpy z%)N9iLZ1s_cu4}CNiDKsMKvDLx<@L_bJwWCy6GwW3y8s_vjaq80lG3gm?aOzs_YbD zO0e571VTe)OZr>|^ad7~_PN$j_mka+U%R^6VFnD`VmC-Ktee7XI>~_-WPi;ULah*F zfC=uSP;Kq)I{;JVU`UximnAsKEMP1j z&4Fr%3@9LHFi;?T0>i*Wcwykb{ii7@ddY#PJZSS&5|2xL8R0=PUCZqM3oB!R4L{;!e~NS_p8l+c!v6*FBH7Ew_E8 zfaLZZ#uK+O(Rj(1r-A(h1&T1hPrE$ z3+u50j~VG{0JuG+7>J<#@$vB?R0B#tq`2o&RM!+)K^&51i-4Cq5LFq595#<0A;yzT z2`^5d4dKve5k(MU2E!WR2T=Fok0&aRhK7gb+}x_Zei4CvLIh=%8*+Ab?9kAW(EI2- zl^$EG9`y@0ZdB>=;(Vw%_|nS!2RJ6d=oT^%xC7Z&ys1a`=+PsjVZkI;;wi2UF%$#` z@=hwmY$nOMU!;WCw3L)0d_P`=$ks5j|5rj6b%J-{&i(r|Jz91ZqF#JupSE!^y;b`EznWe9NaKq9gKJfHS}^AM&6T4Y{90rN}+ z_<3M6W11KsDDjJr=$)4S7uBdp0Ia%e5mzP(2!4XxgC8qq}gOx>UpfbjA zuk6-93?m|~hnhmue@`K?0-{j@{HY#bs;Nm>sGP_%{;x?v0h|au|Mw@l)^APfNuf}hblp`0UmoCW)8tL+r?*3N z3YFYbW5^5Sv>oE0vdlxFhu7Q>FLZ56&B(xpc9KAvEQc5ibrH5ePEH=xytL!HGEznc z?yNd|MHC#ha5p3(KcJfmYxY}!dmeAim?nOgDn1n0M#0fEFM)T$_@X3}y%@|Za{#$5;?f#&VQ47gIui%O z;`BNqYXacgENACO>XIeHR$$TDcFQ^y<`eCz&-*Zv!3&P(Xx(x!ncu50F|IfV=&CmT z2x!X0mxuT!jgRCWRDSx%Op0Qwb(O1}X(S2wg9j2|yBA%tVt5XFF~XYrv&D&=s>Sh z>N~!I(WUYHp~9|gNjhP*#ahsD91WJ?_&g4Ql>WlmVbMbAV6P51ei2t^R8$mmWng3c z*!bH9XlKZiqjf+%lk10}&qFDTq4Na8gl{~3EECyltM0IcU{&IuRd!?Odm>w{&#PVB zJbjc~o?0*`ui8xV01XRdDr)g?rDPRiJz~pN?$2Xz(oMik3>MQ>d4sa z*~`z8bdgWmoF|?9I&WP5DAIh=XI4wkrk(Q@Mhvk5TPlC<=4S}{`1t%@Ud}j{NbHga zU~cbV$UzMrK@d7_d@3s~CC@V0f!0PPgmTZSxvLP_> zbf;J5fNOntuM|2sQZdX*j)x>g^u-Kx4+2^djk5!wy1)Xif2`dI|E8Y)gL!Q9_Dfgi zogXoT6Lt1vK^G^FDi+zLIQQw;9fi6x6r=Q~fu?*FZ86Z-hjALdo}M1+Hkc&0hm96P?}dZ@Jt5QAw{w#7vX-=?y2)#tfM}mEG5|NRk^YMKT``)Xx|i>TYToSQ zVxsTx8p`A*!l-l(Ob9E}Q)r!(_c9w@Xfe$X7E@LWmj~ym_F{R#&KwVR1W$TpCY}6c z75BnB`uh5aRF^=?Aqu<9*Ev}0Y7V~o)+R&V6`Jvmvsxy zv%9#sfSVB8ujN$6Qw}AUxdQ$;Jm{S(tE(Q_8sa}ECksH5Ml521l{0sT$gO6U8VAW8 z-09;}Q`Qn?aiiI@cooFf z2vfGOq7fnPUqL8;EF}|V<}@Itd@e(}6NEGo`Rx(&faf65a9}e>VA*!|ftwOAsEsHo zx!Dz+s%L9^GaHMbW$kEd{<9=;M9D#2f|B5b(Zn zbkSU`twwd{sJHixG6IQp#LCgpY~M{s%y!ln8g9+h1#R5VQkNf7FyP!rAUzEDVAdXk zjQ^k=5BC@>I>F3gX&LdUg6V4;T+8hp9g6l_xO|;_mJ14WvO#ovwd?YWW6t#n3N|XH z@?@T(n{Ab56;(BBPuIpP!19s3W;BYInD(jE78e!@q6{{;PT_IScA&a|aN%JAX;4nS zic+Z_B<18Fex-Sse&gWb$-(FgEHW@}M?X7WOvI~gX$QfAn1&_>^cM=hc>V~G4=vWl zoRI{W{HdE^q`AW(jw_QRpWlP$c0Os4nVIQEU)Hew&MhBYrpz#I+=0hQ&cMfrV1u|g zyV4iT(TRy&pwm`ab>IP;w}O(3=arcEQBD@r6z&I2bUzH8ZjZ}gh^Z*orM@^OiIt9& zt4znLFsXkA^%JfxvHfoF`$Y4_Km4m#uY$*bnwf=VetB70(F_gjjIE$n!RQ_NchEkz z0n@nCzv+QT6RSYL)HwGfq&T5bGn_@?f7(<8UgzT@{(r=%hp(c$e}X9G4o@LqVqsZ< z#tqNTRYA;KL;s=@cSQ65JSaqp$!BkYHf#FSmy{yBHMIHK0EdBIn(;~ok7*3Z{D`X+ zuxpC)&9euSUP>v)4fz`T}Mfd3k>uz9{ID+qTScdgLQOCN&!5G z(0p=z1e2s7##&HTf>z_dyweaS0m)i! zBN5kVzn4dIhL!VFn@bTP7}$Bb7=|QV?jxFDCxWRFVs8UH-7R@-J_$@2h~NS`ot&xZ zt2Hr24IsU+QEeYe+4pId5~}foQ=UZZeA*cN63Etf{3K;$YNl%LkqN=CL~|ePf}lm) z?r%K`kH2E99|!V#ynkP{-=dm%zk4%3FEw$8YU(?dbm%;6y+*?)1e* zph$JVZ9QI-r83*gfZvN?>tMeo_u8AD>cv<)a^ay{@{Qae_yWb zqxsAcm$S(9I-hfgztcn=33xqIPfzOgofyx-(FAi!Y#5FEEi;XV0VcXQ+tWTiJ+%fG zhDjG1O3HT|1prq54brV+4(1F$G>sJzxr=>p#kR^(LXT_xVvN7<#fTA@(y$OH)7jLO z6*#pcv(#}}<1_nI_|+vw*Q}`eVJenRwY6)_qYelAjjSko8uIAj0y`%~$Gw_oG!!i4 z{4Do|CSO?IP$K>L-e+?w(Ji>*Ym>)jW6pSRj_0O$uIG0Bvdprd4AJfvskcrWb&t<* zcv5DveMzmYL(Vedcw+*Hd(3ApS$6NRi?FH{ltoLaIjGgr_KJn?+&~E=V z8F3hx?f!P^_RdKiTQc3ur>9kUmxH|b2n5x+IzIKqJ@&qJnf9dC^=|RZwSD@<)|5!g z3E30LOKO@+P z+B1)xC5QF&j$Qh#l=`!^HIA)s22CY&W9Os3DoEM3i8`ZY*4YpiHU|B zT6{^ekE=Vzr|Sbx&`-xJnFU#1{d?DRLxef|%BH;EF5h}+<&m|+xifEs*1HTaOMTSUlgYP1*#-Y;-0zf9@G7gXi8!;HH4vnvS1OwmniIQTI-^N6 z_!FGCQ>Kf2BImVFJk4h9-rJqHmM^AQ1F^J@N%TDNX=*QH@J1c0AAJ`< z#Zc1>`@Ch#{TZB~p&Y5C_3B$s_uS^YHPl)4V}`81f8$ca(N5sU(7kj-FDYCWYcU)sAz81Esq-=Q2 zR65WDKg%yGB`v-pM@FKd0n+-V<0GwVw{^Gs!BernU*np{xQFrY1+}03{IUIpAV(nO zTVwM=u$e6XY2tZ|cVHk!xv0~#<@aslf|>lj*Hw4iHO#7$2h=j=XWp%u$TB7|)@of; zCax!r2p=(xKS;&WI*X1Q(Z?d7ivk3G&-=kOmHmNb?LaFsyf8VbOHnAqhDFPoYH9`6 zU0PqIHLb?Rq>U&IFY(GAa!QiYUzFZ{^1J&yUrYIjB@l!yLB6HoS8UTO|om-t6KnszkZ*o>5W0 zmJ$V3hG~4iNUp#qj2?}Osy9>ko&M2zy#KH=^nopsh`s>YoNi>X?h9EQV|38rVEB^+ zMj^z?eZSTPCSJ}fgZUUReS#;iqG9||);^*F96VsU7@w%EJ*)aeyZBn->f1oZM{*dh zJI9-sY~PdCE_eBEsOZ>z51!36**=Z6ef3$l?4GHb;oCDURoA&NMh+-FM{>NwdV9fs zqL<~gmUk}QH!_BO4gjd*Bzw6kk;>*8gO?{<;WErXQVtK8JZ6%y!xVcqR4z0f#>L$dPFp z^3dgS|0}&-QuI|$F0R+VN9%mGl-mGXeo0lrv=Ny69ef7-K6i_QeZNUtY}P z1UPfbZ&g3r4=zCv{K?R9w`FRv8QfbP82HC*Cvgbs!*q--Yyxj@0n@oL`Gs}sKQeol zO+8n}1%GB1&E<_!byo)G?u>_4o2#Yc0u84symlu>?#~d?*JbA6ZfzWP=ozRV@n)hw zre7dOv4>NDBXVcJ5nkbppBlde|5lzclHN)@D#UdM)q*U=L3>}F?B8vwYPZytzRHCj zI;5AXVXS;ATHf%q49AmJAsK5&m#XPT#xo+B>5AU3rsosBh3yZh?)>UFrGD~~m0t3P z0Fl^)gfInh#kUfq^XvYGCHvZ=*aSCS{&^eYaC2hZvq4h3U8dA4mV5%fdxcAoUX(RX zi_#h&I@EOSI`6z2#kOsk*(QZAx;Xr@650NSY=VmHFY!1({gNHFvuN~M<=1I%kw&@n zkH`srbr51OuuKyi`TcunYN#qik1w(04|SpDJlTU9dEe=iJGk_Y33iLza-44&skGim zWc?v(@~rE~3Oha4+9&^+2FA+($m&f^#rl*z^Hl;J5HV zgVgCQ@epE>av{WM7#$_-KGQ?6vQ%pW1)$(+E%lL8_hA@=M3AtXqn^G+6OO4s3*!QV zp#G(y>X3K=y?c=V5Cbl32~JQ9!Or6Ht!TP%)F%#%0vQBVq&`cJG7NlgE_Vy!p|6EE zBn%QT=uQYfIbahPm(75?RoEkXp<01toM_?*?%TSW0!lbbZ)yylkni|D>)}6MY!(Tb z7IbpMnT4j=CR>!*imQ$n)>hjpqJwns(A2eky?dF;@~PSJ2wux8c#wE=`J}Y|w0yAdbEU1oD4A+IP~>T5v<} zi*X-@nQG`9zT!CrxnkJW1FQTAh>J7zvvomC+y$stF=v)bM@w4;5x~k&nCSAv%Ut^D z+>;hK69MD5+|ZVX+Ohb;wYRnTPaZxl1y4_@Xv;^bfZQcrzDC0|83()?p8Z`!VX(LV zEi|J5`h>FlTz12VJNyoFO9NHuBP@IUbh~+X@U7R!OeZZjcRrSX&}8#z%1MKDR(N#( zw8MjcZs_mR_v>UxZumqrH?25LOWg=P)KQ_R0=*cjdwF(xR0RqTNWSYEs~*Zpm=*z+ z^2x~s>@tJfBpb>>$fwEXgWKTZ_>E5&EA*02Uacc;)0+6+f+SmO&CZkIOqh*3P9) z68zgzM?pc6f>o-dtZe1tqLHCaB7-yC8pc*^UG#{9^jVr}08RPcSGscMxTxq%#Ax99 z>D4!H#K2NqfW80=3Igy1_(8>NYWIOx7+Adt$sD{?irJL`;GjV$JjMkdhH?esUoeqY zATUDxKp`rDz9&#m{CaNL?mvJ2sP`-vowjG&z$I10&m8-*g@yynv*(8?0|m`w{qL^) zQG9sH`|Y{c6-$9eJ!t`qFe&P2|3oPF zR!*I7yOE_|kOnWIxB$#Ck2V%!_LKrG(0~Glub6;_+?a6i1(}=9kZ4j@?F14 z0kV9(+z%8jBr;ND@uBEiv<(Q^Xk0=%XsdsDgNbPJ zZS|r&gU4U7{hj<`wYBNTET?b66XFjD$*j`eXhB-N6v3Y-`@#Q57jC@5zIN3_ZBmBSLU%bUS8Z1 z)z(Rck``FyLv&{`FtvhH5HKw|D%)zfm5qvv>jJ5I`9GO~e@sw2up`a^+64iR%v<5t zw}rEwd;m;gVKTQ;;^yonLBm(e19doKE_hxA4PI!!HaQM~>>f%=y*@q;j@!UV;TF+Z zR*g(m#*bPjy~&T49E3eAot z^gjHS6=j?aikqAVQTVH&18Mi0&%ufPZWTOoI$)1LLL=pk>r5;xbZl(7H%&I9Z&K~s zN3ST06}v_)JAje^GOZT5DMCU*AQ)=^SK!haOyz?^g>Mug2(!*nf9H?CNP{5mnAQ5t z2bxD3g3yN zGE2K@fD?3r)d5@#x1?k|HliIXbiSx=-m^b%rw%|H#D`!I-)V8A2&A2;1v1O>vpn5De~j z1r0U>9+;w7z!oeG2R5E!n(q#j61Z{yLF@>Xr+`t;VTjX+{Szi9q0=yl`=Owd>>qr2 zX{0rG!{V!WU|=A<=uwW!Cz}hsE?|h!E>RYnU4W$>i(v#M>&#{r1H>heR1t`Dd%Mnf zris-kvXpeVyG9}ZA)TuR1weZ^xpMK*DxdF^z|DQ~88bFoY6l=rf#=H$07bxTtF%hPLLilH6s?$C`*Jf2SNGOd=u`IbHy}FLEK!!h z(_VXAOZ>`KRXE!#OJ9ZWvyWmrqdz7Gp*Fu*nmHx?E02ZsAUj>%4%5F{P`?k)%vfIH zR6u>hjmgAX1Ag~&Sn?jVP%gfF{5>$UN-bu^ zw#o#i`A*RrtW&iZ5M>s6PX8GwiA>BOS;MmR_xbg*=F^rk)Fbt5N%y0C&Q@NF%hFDh zdMCQojOTC@f+<#QGzt}PsK4Fj@~x^|77@BRF#GMOVD#XMS$SNtY5KmBHge>_Eo;mC-$ z_tB(NkwV+}W>31Oe+a+ss&}a?A~JG6*b<{I+XJV~(PzRtBtS(X1tMfdC2CctEJ-7xFCkSl7po-=y$#|g5Dch}B7Nl=+JXf}>f z6G_PTthL3AZ99{H_I6)R+tx>7M8%GbohK!0_&`@Rl%j-o6ayDYmY#h}?)K9k2abkE zJRI)}%e?l;RBg66GjFBml6B6hn3R<5h`$JJ9u7G~WPT@H%# z^?HjZct5MGD(i(9e#!QLg=T5!>lY9}MDKAb@X_)}g5{fc?=qcx-Uaxqf5o0B<2l9e zGjCRQ?=Rptor&nv_dn)~B=;X4QFH0-H6{xcc@pJ^kK*DI4w!$EonfzWw6Ex?|8vZcli1q}=-J19wvHWvvtCfsO9M+FD%P zqpIWsI~YRF(UG!~<==6x=FO|yY8|;da>eN5#Q7`NiBFpU=trjR{NcR&)9%VVukY?~ z45dk(zjcsgYtHxSWqMC?%G9mZBGl&7ocuz9|0~1# z`MVRp+P+Y^00R~>e02$!l5VixFAJGv1Kzki41TDz;gDZJN-gmDJ$cCc($ectxo>W+ zU9pu&>>4c7MaK?xK0l1S_M2Wg>u=nTt?t^T8*Gqu_aVOstNiv4&6TZIR_4hssFRP0Tdk5s-km<$lgT>4{+iKZz@ zItsWd8WHZjP2qltm}Vg(u^Fri(s(HLENh6Cn-Hx5I_@p?$~F1hT-gHW*z$a%XO~M~ z;M{q`mm}4Wzt&Tv9tjdCl+n+Q9H$hO=Q=wBj5W&cnrTK*nPSe?%f+yfk=F9D5kKKq zVR|LjBhpWXm2gHz6iD>y?su40WwnM}_0tSpR8Dl_Zv-BDd z%z1Jot<0Z+nu{&$5o!D;iDk6)jcp*y9Wyf|2<3GRs{VP+<_94fRJCHTTc=}|gP<^= z>HS^Kt3N+%Lks&6N~+#>SDv2ZT^WTFudapl7?Jy(XxkM6Nr09Y+OjlzESlL zYNVbHql}*~x5R$o8R2mkcH|NM%>Z>+6CuGI67 ze*Elab-6j4pA}rvjQ}QOAgbk2k57RN9Zrq}P=A$wWBtyy-&|c3AS7_e7sBNK!mbEf^^cwyzO=$6s8BS!`~0Uk1x`m=-dn2aYKu-&*{D|Rb-v|OIU1zZ0xjzyiJM|!m23^K!$#Bh zt7zZuO6M-S_fJLZN!V6)oON-BG);&_n-9h*%vtTQBK|HZCSXz=_VgKhALi>S#nCa# z@pN@jFKgZ=OtINMhr|(z0Nlk!%9@jaRteEPeSV=B& zHkw?t<+BX{eC@t{d0pW6CA|03?%6T7H(eZiX4v^oCMgf!qq`=37Zc^>4H_#0+7OH7 zbVLpO9L`VbJMJvjStxzr*+1gYl-|X~>wm^u9>3hW`m5gRg;B1Ehbpr&aV|D^=(br} z3dRdJVzGtn4_N$7y(czibR^GZ{$dV!@=qed$Fc3qM6_h2>S7gN1Jou^-0OaBd4PeF zhl5vrBKq#x`VsdpEy-0oD=WVa4=X}EjZftYQ4+oN7V!o$o{RkUkhxM#tIT(jRpl7LB-I zfU!97yPCxBIzT=CVZ75egInAy{n#O=Y*dqmlW&}aeZPp1G96E?l zvtaue>9?}n96fF@akZc?V!`2}#78Pd2Zskojd2-F)VO^VZtjFu%=6FJk8|dxy(j)T zWPfT`|7f&j_3g){k5_CgnGzQ6urSJ~Kld(K+hZA+jI0_5 z$a+mwKaXIMZx|5?3&kay-L+%aK6Yv81hhZutf60=#B9Z&2T+1gc0#-05%C)bp=Cs% zUySTNg`1*7?_$|00w2)f5|S%4;>J@1ri>`1GLkE_xpTHt2C_TK#Ek7!peabkpR!Y! z;4x63As6Puh57&SpJ`2yb$x_#iOAD{_an532#A7^iry7l(9w%;{)6QHpm#XF(9C(> zgvJrw-5iDY?^CEJh%_g;5il4)`*`3b_c`gv6_zg@Q|_VCexWZwh?XN;US0`CPmAt| zQ4JR@f{S+J806i#-$mjYa9IUX9my4F^i^(rKNSbB^y9~@pgn@zdOAC~;4^e~p5%(w;0TBJuiw89I<_Lc3?X;4O57BI zvx7=K&6FJ-9sCw$Cm^m>pT2kR9z3G}PdRmLiLax1<9k^>up?*>VIIhJ?Cj~u1fIaz zKpB{lr<59?2Go{hQE%_yaO~0tvOQe1_q(S24g~!8Po#Nj;ULPfSR-j6N|yIAImVDk%IF&5=1h=1tUz;@nzO`hFQKW0 z!<7s*Emt@K%O&Nvedq)!2zoi4cqJO*;%V~X;vupQ5O`d2vIg{upzwD7L@xq)+9iAY zd-(gBP=+`uZ5-5VQH^)?>(`60fyxAIoSkT0WS=Xji~};xpwvHhpMXW02*q@ zvyldfgc?L`d<`jiHgeCm* zXLV&GPTc2#f@AF}$nQfB3BDo-26J>OwFr#^&d(XwaQd^%h`S6t5H;VvB>_pMD>iEZ zDs%|u@TY!Dy(%)9xNoLyIZ99Qat)*!64WP@k7!M|A(%MB!!qowySoG$GuyeL8Uib| zZ(_occ_&6F0@-&?t1Y6dv0q=Fg9;X)_de*-@5RKlJIm8CFw_8DhyIWgiieX53Rruw zj~=OCHen{?MeOjSN4jMbR`F!#tnUz*+({E@0ks3!}QNIT>&h`1P;+vHF&#CYZUw3vEG%?j{ z{~S8R%lnXt>C*bEKsyspscd88>gUhhr8#bw&dr?)jpEZ1$@C!!7I?T5?_7~|{xU`S zepW|E2RJQKlMO3YezPF+RrH|v0rF>Aj-IKIG#&=9t%!*PKL!J$Gv*JB>y<_MBjF^e z1FN$f$Ip{eHH+N~_)Q-?-@pH1nT>E#e3M3t{4Lcpj{KQt6^mI9sd?|-oosG?==f!N zxa5QUTcG=clLUw>(FbEe_>VdtBv-hp@+fI6U~O9Z)TvX1g&0`g(ZxM7M~{YMrhjX- z;d68!)wp@N?8ao*vlpS3kpq#T)@y$!r%F7+e!V(+Zy#%DW+opLl{=RrA|h(KyGaD? zhUkEbwLd3GwZ!tFZ9czC4yE3Q6&P;3|73HQ7{>|}OWzR;~XdKXoj(2&pZ-s0j{mA4XmGqr5= z%1*rel9Pc(WarX1M38`AuxU$CAd)u`QpXvgM{`#z@OM`lHf0!PlL64Y1+cSS?KB8` zz^(HLE?x*CF(jOevae>er1y^9-PY4x^ZJ^NNYoKczw_s1z5awFlN0J(GFZW_Au4zI zQNfBuFFAJ7h=!ahRa|RPetmWaCK&Jl8=%?LoJ^6@^u1!`-Zy;x_tza(vy2Y~PWR*t zURxGB?%ZSFTHzOe)L7SP%`#v^nMgNWT(kv+LaQu*3fK{zlk~%XEdm8&7#VOs)lMUD zX8(l?^o>on&bb!)x}Do?K8lO)`&t-gW@ED+$(n{~R{dLfdHFRD4?DEqLpxuyQ@lvu zG?xbI2Vu?~bo&gd^H{VQ-ks0sckicWPA^hB@}d391FoE$izB}~%AE8dHhW(19jSEi z>%i^V3Q3``PCSY=SD}BZma1wBwtwLIcOcK^kI(?V{0zs^fA@AMYM@;FOSFf%UjEy$ zch~t8LPC;DuigfCDp&*_4;)P`@s1y#zk*1Jp*<&vFQ`4kw~BZPj)ca!r6oi4d4Q)k zuDYj&=ZSxln5A`|r2Bz3`NwtF{ zOF~QX76Gam4ccHtZfq@?hT7TBJ5>75oO)@%$}~Qi|Jv)yjn0m-UJNIW$w0AjMVtSh z!S1dO4kX+q(Vm%+fr4@yydXmC^!uc|=WX4-zrXF(`s5-D%X#|2U?Df_s>lkz;%n2) zShNOxMDQ9m>JKneAfSW9e-_P5kO}W9_{?Y!r>HfgK5ACkYRqBA>)l2tDpIc|@@6~Q ze5#6MHGY4U19Z3gH!wa*?(T1y+yp49cI%ZU(%@POCr0fgor2B>bvu7lR zt;qrNDhh)u2*EVdy71fZQi`tA2rAJrB+&uEwlt2>L;a`fnz5V0R}`0)c0f3=`m}|G zMRi^6(8w=t9b2~6vPjmDh!89A3hL9K4p9x?z)~GTv?DB82B}&a8n(dwu%9c8H%MXQ zuw)drzC8IQo`0x2H8k*Xwt${Vk?qIz6+{rIw!2|cz#9{YB7mhpdybzXkXP@e66RPR zGSCJYT@|poOf@wzb)?*;w_*sfqGFj(%oe~fpnDmHh~4v)DSHCh%cu+07Y7CgFzl%~ zMf8Azmva01;h*NCl-!zb*1sMe*RFKkFLeIGAMqNGTPBJVGJln;#?VRHUXB~P^1Y-6 zEZQ|sPs7WWAVh!Hgo>xQnTK!+$=*RiZ_|hW(DMouB3a=&k=8y?CO|YajuM4x#E0r< zY2Sv$QlRgby9^rPRILbRripPiCi8Z&uAWSz2xgbw0<~=0YnYf}CLnUCXwUIc1U}bI zQksF_9BeE}ot3C2Yt%=w%zFo;vah^0>8GMH5Om>c;Eflw%Dlv})y}Br&cZE)OGfd{;V& zNp6d2aMXPsuB4-uY25p^KIM#VuHkEDh!s1?c_;#Fab|3X4{M;yA@w2OcBQ|MxzoCa zJ9J{Y|JMB7hG1$N8Jk_wPfzJIh&P8aq!ko;+)f8;YljpE1NZ$3G^bf(`BtF-gM{V3 zw6ru`->X-}7%a0Cb8{{~;8*z6DsQxMP3WRAgX#oa3;mS=Pw~5?r&=`R(>&g(QQ_g- zm`Ef`hH2k(6}GS$dPayd6ldUhmnnPyaG9D2SmYD=RDN>Nz~1q+j{bAWUwr zXK#vzg7aDBIwi2@=ot=+9WzZ^RW7AU7rS*}Ol@!st_p0A)Gc2^Ksx8_* zX#MTY*yN^9nOb?05}e2QR7S*22nO=Nv#zD}BXX4v=##G~y3&^So;MJHj9y&t+SV-% zSY=8*V`3B4^55FY&@k2a7`zb6wNbfYD`B{?A+0jgZ)KKmf4Ka5aMmx|R9#)BZ?6vJ zKOw1HXsW;T>xo?d&H1|*A|uD@Z3QQj0%8amIqm9#FE=qJ&7Ae4^HbmZT+o8rv;X#v zQ$DNljeq`Nzw0cu*X+wNRXMhMcZuQ4bKR|{hjUZdYb9MjcNMud#5G>)EevB|6q!|g zX^3rB?9Yd7BBy<-gWA&=c;furz7Nd=QyVb9%)Y&?K|ny=wRJ{ES2tF@6}Q_nt}JS^ zIDxSww!t>bef(v0_`XQ;-3uNhL44{C?bd-K*A*4_moIkYf6XtEm6bT;Dx}XYEgDt- z%hp;?Z*F5ozN9CM!^wP|u~7*PVOhv8t;N=-x_Y|0$TZY0NP6@z`VlGP?LvQ87?+j^ zpB!IszLiy|CzqVu5gy)(`RhKiD?fs~-DPgKxBqGw(SMt#V{Dci8%qjG%+A*R_R@cY zIA~-a_iEK@TAaa&a4{ii#`=JeRqIr?{*-MQ7Cr~o1+#m5$&@9EGnu z4cmz2AFrakJPr|IO}kM6H|y!%}#SxRzqZq)&^{HoDv9m zYiO8U;uZDtA=4w0aAWvzU5J_{54&W1=f9cGS1!hepks9e1lHSLFiiR^vL6=;D@+i% zw$jdJHqo7i4T5OL>TmhY!oL2(Fw}uiM$sI{T)j-RJr}@CY5#A}7dzHH6jA}x zAtA6p^n_p^h>>iaLO}-$CXR({3JS_J&j@C9H6#K(L9|TAx|}69W#H^McB&uGHCcYS zd*AMk>ywqaHniy%k{a96(5BsYTBvw787BnH4&GsPdJ;RuBXwcg^1{O71lfmiPJJ3_ ztlYB=bZ&lkO(Z5JOq9qubaa5Im}YNa;_8CkLWnAR44vDmc9;@1+ST1i^Uc-Y`wW#_ zKS%pEtF)UclW}|Bwc%Z)&k318m_myX*th@ii8ClFJmHw#H8Z3_h%8-U=&ZOK?ti{m{I_vrI#V**d;)Uc`F9H)48wnp*^ooqier1-U;Lqq?A6C7nx@-a6O zAzx^INf?Ti*T5{aq_g>7Dyo!^F?l~$8~4lhV6x1oc(#CQ#9+5S!Fx*(=+J_O10^k9 z4DFU=q6v@Bf9R0oo;+#6=p5gdsgYRaz@t7poM%I;yfmjF>rP%u%LB9~R(B1CL^Ulf z2Z*cwS5&36OO>twUc~VTq77L(x&%%HIC9eaP$U7q9xr@CF_W#WWP$&Mm|*s!y!p_X4~*SUd*ld_Qivf)Eu9G#e62_gVMI>R(@lLEKi*s-;L zG7^r_)(QtS=^n?Pt!qxo%QZ`k+uR%*-`HXngU&7uOyuDcSO0^E$)t)OD9`p|-BRs4 zh!+q6rf^4oO~!Uh;4YF^I>_T*9o@h0@I~(PYuUQ$_PmzfzHzC!t;LmFwJj|d@`Pn& zAE8z6cx8MH@&RMg&pR^wZE09-d#|;^eyHf6^;9wd&Dd zHd7qV&ufZZ@MKaN-vxf26)bKnl=jX}0(+%BhbKcyXHw}3B9jqaNXR{r@u|~i`0VUX z3@=>VGKnIl?`YSmu4;HNJdUk08W`w@)@kOe_~!)C+yX!FiZB=D1993uSwk#b(ZUOh zWW>t}Al^eB$zu$`g03&kOQl93T~gDE+K=Iyo)yE(l_g7kdR8e{&gQo0>x?4WFAaq= zrinS?;c@4Kcv$oV9noUVKI=N&z5}mHdh`@;s)1Y*y1yeCuF#DB><^-h&$<{>G})7{ zN##6r6DuNNvPZA4Jtlv&mGNe*{MDai5ZqbAwzqSl9~I~pe}w8{^pDr#5K8UZf4Khk zj@|J^xSfM+G#?($RMvg(VeI){+HJkd6>UaKsps6r$2;NwW`CwvR_d~{)U%m)A%Eu{ zQUVD4caJ^|`RnK6Ar=4-5LZYbLkOp%yfKXYN+SLUTDYp_E`8=4vxdDPd!O0;{x!Q+ zd|lG4IQGNh4Tyst%J~*`(5TnXyADOg$X-W#e90*=AFi7h%0%oZYxeiIXNv%AUby7tNz%!EvUli>LzvZw(K z9{f6rM;LD`y6=Hl(O0=zxf9*vrlnGa=O5@h9W~hJp9A)mR4ogdyHT3ud>Z8q7>N*)PmSa?aCt(_DSkRQ1#rMtcwvOwCtEwm{s790=`MpxpuD!Y#bNkLN1S{5`t-Qe= zQ2P>dAt2GI{^B?U4wfw(2ZCM3)r>WW$p;qqaKLCCh&LkUIVdD8injo{;3d$5)$M8h z@BLVSqm6#~qW`Aoc`AuP3WydErX`mmS9g2f{|{mE1;TVe^$c&EPU$(S3YQ;Gpzi6& z(n&LVa*UHR5Gci4B(@VmEIgz{!?aQkY@Pq^Z@$%0y!_=5qPuW1gzFJ=lE#4cZaxu_ zNH7!!=;@WvR&(6G4fShWZ0uQg^}S^F&z}QTvvzgW9C=}I?Z%CCFnE%#eTxwWJZQ%V z|0`i1tgXEV2*A86hd{HOv{H9NmS%(Ebkw|r-p0nJ0s>KhL#3PnJ_yehfP>iqk0Uf^ z63Apla7cJej804~2Swn2MLescNRBl|Mm~N^MI@mUub%^70HSb2h2=Y>@xiFSrbR9v z>*{vnolX#Ze!|YIBW7hx0U3DR0e4(Em zhZ+=QLLF1p6TG+vIy3}J@$m7D7C!An(|2oS9`;(`H4G?P=P+7USA7p; z&&t<2*3QTaK_wW2noeH;sbp{>5qAkDD@0+m_w*#303v+XOG0m^dF>W6GR%Q3M}u#n vAt%x8G(?B>|I#Vcs?o9(`2WE)wJmuH8@GK<^wAV#_@}0PK`H0#rNI9K4Mz=P literal 0 HcmV?d00001 diff --git a/tests/visualization/test_graphviz.py b/tests/visualization/test_graphviz.py index 0010c67..8ae9fb3 100644 --- a/tests/visualization/test_graphviz.py +++ b/tests/visualization/test_graphviz.py @@ -33,9 +33,6 @@ def test_graphviz_examples(example_file): rendered_graph = workflow.render_graph( filename=f"tests/visualization/outputs/{example_name}.dot" ) - rendered_graph = workflow.render_graph( - filename=f"tests/visualization/outputs/{example_name}.png" - ) # Render PNG visualizations for comparison fixture_dot = Path(f"tests/visualization/fixtures/{example_name}.dot") @@ -52,6 +49,19 @@ def test_graphviz_examples(example_file): except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f" Warning: Could not render {fixture_png}: {e}") + output_filename = Path(f"tests/visualization/outputs/{example_name}.dot") + if output_filename.exists(): + output_png = output_filename.with_suffix(".png") + try: + subprocess.run( + ["dot", "-Tpng", str(output_filename), "-o", str(output_png)], + check=True, + capture_output=True, + ) + print(f" Generated: {output_png}") + except (subprocess.CalledProcessError, FileNotFoundError) as e: + print(f" Warning: Could not render {output_png}: {e}") + with open(f"tests/visualization/fixtures/{example_name}.dot", encoding="utf-8") as f: expected_graph = f.read() From db70bc65ce2d3b2fd59fae170558a4488855c798 Mon Sep 17 00:00:00 2001 From: Armin Graf Date: Mon, 19 Jan 2026 08:04:12 -0500 Subject: [PATCH 3/3] fix: ruff formatting Signed-off-by: Armin Graf --- tests/visualization/test_graphviz.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/visualization/test_graphviz.py b/tests/visualization/test_graphviz.py index 8ae9fb3..b7dd7bf 100644 --- a/tests/visualization/test_graphviz.py +++ b/tests/visualization/test_graphviz.py @@ -2,6 +2,7 @@ Tests for graphviz visualization of Serverless Workflow spec examples. """ + import subprocess from pathlib import Path