diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1edd27a..72b690c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: markdown: ${{ steps.changes.outputs.markdown_all }} steps: - name: "Check out repository code" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" - name: Check for file changes uses: opsmill/paths-filter@v3.0.2 id: changes @@ -44,7 +44,7 @@ jobs: timeout-minutes: 5 steps: - name: "Check out repository code" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" - name: Install uv uses: astral-sh/setup-uv@v7 with: @@ -66,7 +66,7 @@ jobs: timeout-minutes: 5 steps: - name: "Check out repository code" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" - name: Install uv uses: astral-sh/setup-uv@v7 with: @@ -82,7 +82,7 @@ jobs: timeout-minutes: 5 steps: - name: "Check out repository code" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" - uses: actions/setup-node@v6 with: node-version: 20 @@ -91,32 +91,36 @@ jobs: - name: "Run markdownlint" run: markdownlint "**/*.{md,mdx}" - # integration-test: - # needs: ["python-lint", "yaml-lint", "markdown-lint"] - # runs-on: - # group: "huge-runners" - # strategy: - # fail-fast: false - # matrix: - # include: - # - os: ubuntu-latest - # timeout-minutes: 60 - # env: - # INFRAHUB_DB_TYPE: neo4j - # INFRAHUB_API_TOKEN: '06438eb2-8019-4776-878c-0941b1f1d1ec' - # INFRAHUB_TIMEOUT: 600 - # steps: - # - name: "Check out repository code" - # uses: "actions/checkout@v5" - # - name: Install uv - # uses: astral-sh/setup-uv@v7 - # with: - # version: "0.9.18" - # - run: uv sync - # - name: "Run tests" - # run: uv run pytest tests/ - # env: - # REPOSITORY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + integration-test: + if: | + always() && !cancelled() && + !contains(needs.*.result, 'failure') && + !contains(needs.*.result, 'cancelled') + needs: ["files-changed", "python-lint", "yaml-lint", "markdown-lint"] + runs-on: + group: "huge-runners" + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + timeout-minutes: 60 + env: + INFRAHUB_DB_TYPE: neo4j + INFRAHUB_API_TOKEN: '06438eb2-8019-4776-878c-0941b1f1d1ec' + INFRAHUB_TIMEOUT: 600 + steps: + - name: "Check out repository code" + uses: "actions/checkout@v6" + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.9.18" + - run: uv sync + - name: "Run tests" + run: uv run pytest tests/ -v + env: + REPOSITORY_TOKEN: ${{ secrets.GITHUB_TOKEN }} documentation: defaults: @@ -132,7 +136,7 @@ jobs: timeout-minutes: 5 steps: - name: "Check out repository code" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" with: submodules: true - name: Install NodeJS @@ -162,7 +166,7 @@ jobs: timeout-minutes: 5 steps: - name: "Check out repository code" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" with: submodules: true diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index 2b86c30..b5ed5cf 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: source-repo - name: Checkout target repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: opsmill/infrahub-docs token: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/update-infrahub-sdk.yml b/.github/workflows/update-infrahub-sdk.yml index 9193b4a..097bf80 100644 --- a/.github/workflows/update-infrahub-sdk.yml +++ b/.github/workflows/update-infrahub-sdk.yml @@ -35,7 +35,7 @@ jobs: BRANCH_NAME: ${{ matrix.branch-name }}-infrahub-sdk-${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version || github.event.inputs.version }} steps: - name: Check out code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/update-infrahub.yml b/.github/workflows/update-infrahub.yml index f96e190..fe18120 100644 --- a/.github/workflows/update-infrahub.yml +++ b/.github/workflows/update-infrahub.yml @@ -41,7 +41,7 @@ jobs: BRANCH_NAME: ${{ matrix.branch-name }}-${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version || github.event.inputs.version }} steps: - name: Check out code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/Dockerfile b/Dockerfile index 736f3f8..db84901 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,15 @@ # This Dockerfile serves two purposes: # 1. It builds a custom Infrahub image with the `service_catalog` python module included. It can now be imported and used within the Infrahub environment (in generators for example). # 2. It builds a container that runs streamlit to serve the service catalog web application. -ARG INFRAHUB_BASE_VERSION=1.4.3 +ARG INFRAHUB_BASE_VERSION=1.6.2 FROM registry.opsmill.io/opsmill/infrahub:${INFRAHUB_BASE_VERSION} WORKDIR /opt/local -COPY pyproject.toml poetry.lock README.md ./ +COPY pyproject.toml uv.lock README.md ./ COPY service_catalog/ service_catalog/ -RUN poetry install --no-ansi --no-interaction +# Install service_catalog package and its dependencies into the existing Infrahub venv +# Using uv pip install instead of uv sync to avoid overwriting Infrahub dependencies +RUN uv pip install -e . -WORKDIR /source \ No newline at end of file +WORKDIR /source diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 0892976..e21413b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,11 +1,11 @@ --- x-infrahub-custom-build: &infrahub_custom_build - image: opsmill/infrahub-demo-service-catalog:${INFRAHUB_BASE_VERSION:-1.4.3} + image: opsmill/infrahub-demo-service-catalog:${INFRAHUB_BASE_VERSION:-1.6.2} build: context: . dockerfile: Dockerfile args: - INFRAHUB_BASE_VERSION: "${INFRAHUB_BASE_VERSION:-1.4.3}" + INFRAHUB_BASE_VERSION: "${INFRAHUB_BASE_VERSION:-1.6.2}" services: service-catalog: @@ -15,7 +15,7 @@ services: INFRAHUB_API_TOKEN: ${INFRAHUB_API_TOKEN:-44af444d-3b26-410d-9546-b758657e026c} ports: - 8501:8501 - command: poetry run streamlit run /opt/local/service_catalog/🏠_Home_Page.py + command: uv run streamlit run /opt/local/service_catalog/🏠_Home_Page.py infrahub-server: <<: *infrahub_custom_build task-worker: diff --git a/docker-compose.yml b/docker-compose.yml index e198c87..5b1f50c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ x-infrahub-config: &infrahub_config INFRAHUB_BROKER_USERNAME: &broker_username ${INFRAHUB_BROKER_USERNAME:-infrahub} INFRAHUB_BROKER_VIRTUALHOST: ${INFRAHUB_BROKER_VIRTUALHOST:-/} INFRAHUB_CACHE_ADDRESS: ${INFRAHUB_CACHE_ADDRESS:-localhost} + INFRAHUB_CACHE_CLEAN_UP_DEADLOCKS_INTERVAL_MINS: ${INFRAHUB_CACHE_CLEAN_UP_DEADLOCKS_INTERVAL_MINS:-15} INFRAHUB_CACHE_DATABASE: ${INFRAHUB_CACHE_DATABASE:-0} INFRAHUB_CACHE_DRIVER: ${INFRAHUB_CACHE_DRIVER:-redis} INFRAHUB_CACHE_ENABLE: ${INFRAHUB_CACHE_ENABLE:-true} @@ -68,8 +69,13 @@ x-infrahub-config: &infrahub_config INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS: ${INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS:-false} INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX: ${INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX:-false} INFRAHUB_GIT_APPEND_GIT_SUFFIX: + INFRAHUB_GIT_GLOBAL_CONFIG_FILE: ${INFRAHUB_GIT_GLOBAL_CONFIG_FILE:-/opt/infrahub/.gitconfig} + INFRAHUB_GIT_IMPORT_SYNC_BRANCH_NAMES: INFRAHUB_GIT_REPOSITORIES_DIRECTORY: ${INFRAHUB_GIT_REPOSITORIES_DIRECTORY:-repositories} INFRAHUB_GIT_SYNC_INTERVAL: ${INFRAHUB_GIT_SYNC_INTERVAL:-10} + INFRAHUB_GIT_USER_EMAIL: ${INFRAHUB_GIT_USER_EMAIL:-infrahub@opsmill.com} + INFRAHUB_GIT_USER_NAME: ${INFRAHUB_GIT_USER_NAME:-Infrahub} + INFRAHUB_GIT_USE_EXPLICIT_MERGE_COMMIT: ${INFRAHUB_GIT_USE_EXPLICIT_MERGE_COMMIT:-false} INFRAHUB_HTTP_TIMEOUT: ${INFRAHUB_HTTP_TIMEOUT:-10} INFRAHUB_HTTP_TLS_CA_BUNDLE: INFRAHUB_HTTP_TLS_INSECURE: ${INFRAHUB_HTTP_TLS_INSECURE:-false} @@ -121,6 +127,7 @@ x-infrahub-config: &infrahub_config INFRAHUB_WORKFLOW_ENABLE: ${INFRAHUB_WORKFLOW_ENABLE:-true} INFRAHUB_WORKFLOW_EXTRA_LOGGERS: INFRAHUB_WORKFLOW_EXTRA_LOG_LEVEL: ${INFRAHUB_WORKFLOW_EXTRA_LOG_LEVEL:-INFO} + INFRAHUB_WORKFLOW_FLOW_RUN_COUNT_CACHE_THRESHOLD: ${INFRAHUB_WORKFLOW_FLOW_RUN_COUNT_CACHE_THRESHOLD:-100000} INFRAHUB_WORKFLOW_PORT: INFRAHUB_WORKFLOW_TLS_ENABLED: ${INFRAHUB_WORKFLOW_TLS_ENABLED:-false} INFRAHUB_WORKFLOW_WORKER_POLLING_INTERVAL: ${INFRAHUB_WORKFLOW_WORKER_POLLING_INTERVAL:-2} @@ -170,7 +177,7 @@ services: - 15692:15692 cache: - image: ${CACHE_DOCKER_IMAGE:-redis:7.2.4} + image: ${CACHE_DOCKER_IMAGE:-redis:7.2.11} restart: unless-stopped healthcheck: test: ["CMD-SHELL", "redis-cli ping | grep PONG"] @@ -199,7 +206,7 @@ services: - 6362:6362 task-manager: - image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.3}" + image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.6.2}" command: uvicorn --host 0.0.0.0 --port 4200 --factory infrahub.prefect_server.app:create_infrahub_prefect restart: unless-stopped depends_on: @@ -208,7 +215,7 @@ services: environment: PREFECT_API_DATABASE_CONNECTION_URL: postgresql+asyncpg://${INFRAHUB_TASKMANAGER_DB_USER:-postgres}:${INFRAHUB_TASKMANAGER_DB_PASSWORD:-postgres}@task-manager-db:5432/${INFRAHUB_TASKMANAGER_DB_DATABASE:-prefect} healthcheck: - test: /usr/local/bin/httpx http://localhost:4200/api/health || exit 1 + test: curl -s -f -o /dev/null http://localhost:4200/api/health || exit 1 interval: 5s timeout: 5s retries: 20 @@ -232,7 +239,7 @@ services: retries: 5 infrahub-server: - image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.3}" + image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.6.2}" restart: unless-stopped command: > gunicorn --config backend/infrahub/serve/gunicorn_config.py @@ -278,7 +285,7 @@ services: deploy: mode: replicated replicas: 2 - image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.3}" + image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.6.2}" command: prefect worker start --type infrahubasync --pool infrahub-worker --with-healthcheck restart: unless-stopped depends_on: diff --git a/docs/docs/getting-started/installation.mdx b/docs/docs/getting-started/installation.mdx index 1e84e86..fe08792 100644 --- a/docs/docs/getting-started/installation.mdx +++ b/docs/docs/getting-started/installation.mdx @@ -28,10 +28,10 @@ Before you get started, make sure you have these tools installed: - Or Docker Engine on a Linux VM for server deployments - Minimum 4GB RAM allocated to Docker -- 🐍 **Poetry**: Python dependency management tool - - Install via the [official installation guide](https://python-poetry.org/docs) +- 🐍 **uv**: Fast Python package and project manager + - Install via the [official installation guide](https://docs.astral.sh/uv/getting-started/installation/) - Used to manage Python packages and virtual environments - - Version 1.2.0 or higher recommended + - Version 0.4.0 or higher recommended ### System requirements @@ -81,10 +81,10 @@ export INFRAHUB_API_TOKEN="06438eb2-8019-4776-878c-0941b1f1d1ec" ### Step 3: Install Python dependencies -Use Poetry to create a virtual environment and install all required Python packages. +Use uv to create a virtual environment and install all required Python packages. ```shell -poetry install +uv sync ``` This command: @@ -94,8 +94,8 @@ This command: - Installs Streamlit for the web portal interface - Sets up development tools (pytest, ruff, mypy) -:::note First Time Using Poetry? -Poetry automatically manages virtual environments. All subsequent commands should be run with `poetry run` prefix to ensure they use the correct environment. +:::note First time using uv? +uv automatically manages virtual environments. All subsequent commands should be run with `uv run` prefix to ensure they use the correct environment. ::: ### Step 4: Start the application stack @@ -103,7 +103,7 @@ Poetry automatically manages virtual environments. All subsequent commands shoul Launch Infrahub and the Service Catalog using Docker Compose. This single command starts multiple services. ```shell -poetry run invoke start +uv run invoke start ``` :::important Why Use Invoke? @@ -130,7 +130,7 @@ This command launches: Connect the demo repository to Infrahub for infrastructure as code workflows. ```shell -poetry run infrahubctl repository add --ref main --read-only infrahub-demo https://github.com/opsmill/infrahub-demo-service-catalog.git +uv run infrahubctl repository add --ref main --read-only infrahub-demo https://github.com/opsmill/infrahub-demo-service-catalog.git ``` #### Why Git integration? @@ -196,7 +196,7 @@ Confirm everything is working by accessing both interfaces: - Ensure Docker is running and has sufficient resources - Check for port conflicts (8000, 8501, 5432, 6379) -- Run `poetry run invoke destroy` then `poetry run invoke start` to reset +- Run `uv run invoke destroy` then `uv run invoke start` to reset ### Schema load fails diff --git a/generators/implement_dedicated_internet.py b/generators/implement_dedicated_internet.py index 5eb597b..3b9e710 100644 --- a/generators/implement_dedicated_internet.py +++ b/generators/implement_dedicated_internet.py @@ -6,14 +6,27 @@ from infrahub_sdk.generator import InfrahubGenerator from infrahub_sdk.node import InfrahubNode from infrahub_sdk.protocols import CoreIPPrefixPool, CoreNumberPool -from service_catalog.protocols_async import ( - DcimDevice, - DcimInterfaceL3, - IpamIPAddress, - IpamPrefix, - IpamVLAN, - ServiceDedicatedInternet, -) + +try: + # When imported as part of a package (e.g., within Infrahub) + from .protocols import ( + DcimDevice, + DcimInterfaceL3, + IpamIPAddress, + IpamPrefix, + IpamVLAN, + ServiceDedicatedInternet, + ) +except ImportError: + # When imported directly (e.g., in unit tests) + from protocols import ( # type: ignore[no-redef] + DcimDevice, + DcimInterfaceL3, + IpamIPAddress, + IpamPrefix, + IpamVLAN, + ServiceDedicatedInternet, + ) ACTIVE_STATUS = "active" SERVICE_VLAN_POOL: str = "Customer vlan pool" diff --git a/generators/protocols.py b/generators/protocols.py new file mode 100644 index 0000000..20725a7 --- /dev/null +++ b/generators/protocols.py @@ -0,0 +1,219 @@ +# +# Protocol definitions for Infrahub generators +# These are duplicated here to allow generators to run within Infrahub's +# environment without requiring the service_catalog package to be installed. +# +# Generated by "infrahubctl protocols" and trimmed to only include types +# needed by the generators. +# + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from infrahub_sdk.protocols import ( + BuiltinIPAddress, + BuiltinIPPrefix, + CoreArtifactTarget, + CoreNode, +) + +if TYPE_CHECKING: + from infrahub_sdk.node import RelatedNode, RelationshipManager + from infrahub_sdk.protocols_base import ( + BooleanOptional, + Dropdown, + DropdownOptional, + Integer, + IntegerOptional, + IPHost, + IPNetwork, + String, + StringOptional, + ) + + +class DcimGenericDevice(CoreNode): + description: StringOptional + name: String + os_version: StringOptional + interfaces: RelationshipManager + member_of_groups: RelationshipManager + platform: RelatedNode + primary_address: RelatedNode + profiles: RelationshipManager + subscriber_of_groups: RelationshipManager + tags: RelationshipManager + + +class DcimPhysicalDevice(CoreNode): + position: IntegerOptional + rack_face: DropdownOptional + serial: StringOptional + device_type: RelatedNode + location: RelatedNode + member_of_groups: RelationshipManager + profiles: RelationshipManager + subscriber_of_groups: RelationshipManager + + +class DcimEndpoint(CoreNode): + connector: RelatedNode + member_of_groups: RelationshipManager + profiles: RelationshipManager + subscriber_of_groups: RelationshipManager + + +class DcimInterface(CoreNode): + description: StringOptional + enabled: BooleanOptional + mtu: IntegerOptional + name: String + speed: Integer + device: RelatedNode + member_of_groups: RelationshipManager + profiles: RelationshipManager + service: RelatedNode + subscriber_of_groups: RelationshipManager + tags: RelationshipManager + + +class ServiceGeneric(CoreNode): + account_reference: String + service_identifier: String + member_of_groups: RelationshipManager + profiles: RelationshipManager + subscriber_of_groups: RelationshipManager + + +class DcimDevice(CoreArtifactTarget, DcimGenericDevice, DcimPhysicalDevice): + description: StringOptional + index: IntegerOptional + name: String + os_version: StringOptional + position: IntegerOptional + rack_face: DropdownOptional + role: DropdownOptional + serial: StringOptional + status: Dropdown + artifacts: RelationshipManager + device_type: RelatedNode + interfaces: RelationshipManager + location: RelatedNode + member_of_groups: RelationshipManager + object_template: RelatedNode + platform: RelatedNode + primary_address: RelatedNode + profiles: RelationshipManager + subscriber_of_groups: RelationshipManager + tags: RelationshipManager + + +class DcimInterfaceL2(DcimInterface, DcimEndpoint): + description: StringOptional + enabled: BooleanOptional + l2_mode: StringOptional + mtu: IntegerOptional + name: String + role: DropdownOptional + speed: Integer + status: DropdownOptional + connector: RelatedNode + device: RelatedNode + member_of_groups: RelationshipManager + profiles: RelationshipManager + service: RelatedNode + subscriber_of_groups: RelationshipManager + tagged_vlan: RelationshipManager + tags: RelationshipManager + untagged_vlan: RelatedNode + + +class DcimInterfaceL3(DcimInterface, DcimEndpoint): + description: StringOptional + enabled: BooleanOptional + mtu: IntegerOptional + name: String + role: DropdownOptional + speed: Integer + status: DropdownOptional + connector: RelatedNode + device: RelatedNode + ip_addresses: RelationshipManager + member_of_groups: RelationshipManager + profiles: RelationshipManager + service: RelatedNode + subscriber_of_groups: RelationshipManager + tags: RelationshipManager + + +class IpamIPAddress(BuiltinIPAddress): + address: IPHost + description: StringOptional + fqdn: StringOptional + interface: RelatedNode + ip_namespace: RelatedNode + ip_prefix: RelatedNode + member_of_groups: RelationshipManager + profiles: RelationshipManager + service: RelatedNode + subscriber_of_groups: RelationshipManager + + +class IpamPrefix(BuiltinIPPrefix): + broadcast_address: StringOptional + description: StringOptional + hostmask: StringOptional + is_pool: BooleanOptional + is_top_level: BooleanOptional + member_type: DropdownOptional + netmask: StringOptional + network_address: StringOptional + prefix: IPNetwork + role: DropdownOptional + status: Dropdown + utilization: IntegerOptional + children: RelationshipManager + gateway: RelatedNode + ip_addresses: RelationshipManager + ip_namespace: RelatedNode + location: RelatedNode + member_of_groups: RelationshipManager + organization: RelatedNode + parent: RelatedNode + profiles: RelationshipManager + resource_pool: RelationshipManager + service: RelatedNode + subscriber_of_groups: RelationshipManager + vlan: RelatedNode + + +class IpamVLAN(CoreNode): + description: StringOptional + name: String + role: DropdownOptional + status: Dropdown + vlan_id: Integer + l2domain: RelatedNode + location: RelationshipManager + member_of_groups: RelationshipManager + prefixes: RelationshipManager + profiles: RelationshipManager + service: RelatedNode + subscriber_of_groups: RelationshipManager + + +class ServiceDedicatedInternet(ServiceGeneric): + account_reference: String + bandwidth: Dropdown + ip_package: Dropdown + service_identifier: String + status: DropdownOptional + dedicated_interfaces: RelationshipManager + gateway_ip_address: RelatedNode + location: RelatedNode + member_of_groups: RelationshipManager + prefix: RelatedNode + profiles: RelationshipManager + subscriber_of_groups: RelationshipManager + vlan: RelatedNode diff --git a/pyproject.toml b/pyproject.toml index 63b18f0..0d88e31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,22 +10,22 @@ dependencies = [ "infrahub-sdk[all]>=1.17.0,<2.0.0", "streamlit>=1.51.0,<2.0.0", "watchdog>=6.0.0,<7.0.0", - "fast-depends>=2.4.12,<3.0.0", + "fast-depends>=3.0.3,<4.0.0", "invoke>=2.2.1,<3.0.0", ] [dependency-groups] dev = [ "mypy>=1.19.1", - "pytest>=9.0.0", - "pytest-asyncio>=1.3.0", - "pytest-httpx>=0.35.0", + "pytest>=8.0.0,<9", + "pytest-asyncio>=0.23.0,<1", + "pytest-httpx>=0.30.0,<0.35", "pytest-clarity>=1.0.1", - "ruff>=0.14.0", + "ruff>=0.14.4", "types-pyyaml>=6.0.12", "types-requests>=2.32.0", "yamllint>=1.37.1", - "infrahub-testcontainers>=1.4.5", + "infrahub-testcontainers>=1.6.2", ] [tool.pytest.ini_options] diff --git a/service_catalog/infrahub.py b/service_catalog/infrahub.py index 89f7df2..eb9db85 100644 --- a/service_catalog/infrahub.py +++ b/service_catalog/infrahub.py @@ -40,7 +40,7 @@ def create_branch(branch_name: str, client: InfrahubClientSync = Depends(get_cli return client.branch.create(branch_name=branch_name, sync_with_git=False) -@inject(cast=False) # type: ignore[call-overload] +@inject(cast=False) # type: ignore[arg-type] def create_and_save( kind: type[SchemaTypeSync], data: dict[str, Any], @@ -56,7 +56,7 @@ def create_and_save( return infrahub_node -@inject(cast=False) # type: ignore[call-overload] +@inject(cast=False) # type: ignore[arg-type] def filter_nodes( kind: type[SchemaTypeSync], filters: dict[str, Any] | None = None, @@ -76,7 +76,7 @@ def filter_nodes( return result -@inject(cast=False) # type: ignore[call-overload] +@inject(cast=False) # type: ignore[arg-type] def get_dropdown_options( kind: str | type[SchemaTypeSync], attribute_name: str, diff --git "a/service_catalog/pages/1_\360\237\224\214_Dedicated_Internet.py" "b/service_catalog/pages/1_\360\237\224\214_Dedicated_Internet.py" index 3338823..39b34e3 100644 --- "a/service_catalog/pages/1_\360\237\224\214_Dedicated_Internet.py" +++ "b/service_catalog/pages/1_\360\237\224\214_Dedicated_Internet.py" @@ -83,7 +83,7 @@ def get_locations_options() -> list[str]: "member_of_groups": ["automated_dedicated_internet"], "location": [location], } - service_obj = create_and_save( + service_obj: ServiceDedicatedInternet = create_and_save( kind=ServiceDedicatedInternet, data=service, branch=branch_name, @@ -98,7 +98,7 @@ def get_locations_options() -> list[str]: "tags": ["service_request"], } - proposed_change_obj = create_and_save( + proposed_change_obj: CoreProposedChange = create_and_save( kind=CoreProposedChange, data=proposed_change, ) diff --git a/tests/integration/test_create_service.py b/tests/integration/test_create_service.py index 246b230..a4b4fed 100644 --- a/tests/integration/test_create_service.py +++ b/tests/integration/test_create_service.py @@ -6,7 +6,7 @@ from streamlit.testing.v1 import AppTest from infrahub_sdk.client import InfrahubClient, InfrahubClientSync -from infrahub_sdk.protocols import CoreGenericRepository +from infrahub_sdk.protocols import CoreGenericRepository, CoreProposedChange from infrahub_sdk.spec.object import ObjectFile from infrahub_sdk.testing.docker import TestInfrahubDockerClient from infrahub_sdk.testing.repository import GitRepo @@ -93,15 +93,35 @@ async def test_add_repository( async def test_portal(self, override_client: None, client: InfrahubClient, default_branch: str) -> None: """ Test the streamlit app on top of a running infrahub instance. + + Verifies that submitting the form: + 1. Creates a new branch + 2. Creates a service object on that branch + 3. Creates a proposed change targeting main """ + service_identifier = "test-12345" + expected_branch_name = f"implement_{service_identifier.lower()}" + app = AppTest.from_file("service_catalog/pages/1_🔌_Dedicated_Internet.py").run() - app.text_input("input-service-identifier").set_value("test-12345").run() - app.text_input("input-account-reference").set_value("test-12345").run() + app.text_input("input-service-identifier").set_value(service_identifier).run() + app.text_input("input-account-reference").set_value("acct-12345").run() app.selectbox("select-location").select("bru01").run() app.selectbox("select-bandwidth").set_value(100).run() app.select_slider("select-ip-package").set_value("small").run() app.button("FormSubmitter:new_dedicated_internet_form-Submit").click().run(timeout=15) - services = await client.all(kind=ServiceDedicatedInternet, branch=default_branch) - assert len(services) == 1 + # Verify the branch was created + branches = await client.branch.all() + assert expected_branch_name in branches, f"Branch '{expected_branch_name}' was not created" + + # Verify the service was created on the new branch + services = await client.all(kind=ServiceDedicatedInternet, branch=expected_branch_name) + assert len(services) == 1, "Service was not created on the new branch" + assert services[0].service_identifier.value == service_identifier + + # Verify the proposed change was created + proposed_changes = await client.all(kind=CoreProposedChange) + assert len(proposed_changes) == 1, "Proposed change was not created" + assert proposed_changes[0].source_branch.value == expected_branch_name + assert proposed_changes[0].destination_branch.value == default_branch diff --git a/uv.lock b/uv.lock index 5911997..2350350 100644 --- a/uv.lock +++ b/uv.lock @@ -86,15 +86,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" }, ] -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, -] - [[package]] name = "attrs" version = "25.4.0" @@ -117,15 +108,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483, upload-time = "2024-03-13T03:41:26.969Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "black" version = "25.12.0" @@ -354,15 +336,15 @@ wheels = [ [[package]] name = "fast-depends" -version = "2.4.12" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "pydantic" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682, upload-time = "2024-10-16T17:44:35.963Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/f3/41e955f5f0811de6ef9f00f8462f2ade7bc4a99b93714c9b134646baa831/fast_depends-3.0.5.tar.gz", hash = "sha256:c915a54d6e0d0f0393686d37c14d54d9ec7c43d7b9def3f3fc4f7b4d52f67f2a", size = 18235, upload-time = "2025-11-30T20:26:12.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651, upload-time = "2024-10-16T17:44:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/76697228ae63dcbaf0a0a1b20fc996433a33f184ac4f578382b681dcf5ea/fast_depends-3.0.5-py3-none-any.whl", hash = "sha256:38a3d7044d3d6d0b1bed703691275c870316426e8a9bfa6b1c89e979b15659e2", size = 25362, upload-time = "2025-11-30T20:26:10.96Z" }, ] [[package]] @@ -489,17 +471,18 @@ wheels = [ [[package]] name = "httpx" -version = "0.28.1" +version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, + { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, ] [package.optional-dependencies] @@ -574,7 +557,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "fast-depends", specifier = ">=2.4.12,<3.0.0" }, + { name = "fast-depends", specifier = ">=3.0.3,<4.0.0" }, { name = "infrahub-sdk", extras = ["all"], specifier = ">=1.17.0,<2.0.0" }, { name = "invoke", specifier = ">=2.2.1,<3.0.0" }, { name = "streamlit", specifier = ">=1.51.0,<2.0.0" }, @@ -583,13 +566,13 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "infrahub-testcontainers", specifier = ">=1.4.5" }, + { name = "infrahub-testcontainers", specifier = ">=1.6.2" }, { name = "mypy", specifier = ">=1.19.1" }, - { name = "pytest", specifier = ">=9.0.0" }, - { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest", specifier = ">=8.0.0,<9" }, + { name = "pytest-asyncio", specifier = ">=0.23.0,<1" }, { name = "pytest-clarity", specifier = ">=1.0.1" }, - { name = "pytest-httpx", specifier = ">=0.35.0" }, - { name = "ruff", specifier = ">=0.14.0" }, + { name = "pytest-httpx", specifier = ">=0.30.0,<0.35" }, + { name = "ruff", specifier = ">=0.14.4" }, { name = "types-pyyaml", specifier = ">=6.0.12" }, { name = "types-requests", specifier = ">=2.32.0" }, { name = "yamllint", specifier = ">=1.37.1" }, @@ -631,7 +614,7 @@ all = [ [[package]] name = "infrahub-testcontainers" -version = "1.5.5" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -641,9 +624,9 @@ dependencies = [ { name = "pytest" }, { name = "testcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/ff/76557304631d521337b705373acbda0cf89a4606827357610ceda5d990d0/infrahub_testcontainers-1.5.5.tar.gz", hash = "sha256:5ba46dde666fad94c8c246ea086b957de63f06ddb7b8ac92ea5881f104068527", size = 16095, upload-time = "2025-12-22T19:37:30.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/4e/7dcde84b7becf5f9af8f8394e50c3cbb2284b49d43f74ac515de954d5f44/infrahub_testcontainers-1.6.2.tar.gz", hash = "sha256:14e30b5fd1b1c8439164026cdb39cf6c7ded844d9f203283759947294f3f0177", size = 16397, upload-time = "2025-12-22T19:18:10.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/12/e55163d1208085dde87bd8756a827ed5a3c33bb2e9d1e9e92156746489a3/infrahub_testcontainers-1.5.5-py3-none-any.whl", hash = "sha256:04de4367a267a0f182618155aeb1bb36d5ca6258773480e3337327e217965738", size = 23109, upload-time = "2025-12-22T19:37:29.041Z" }, + { url = "https://files.pythonhosted.org/packages/28/ab/a0e349742bf43ea8e0ea8e7cc18546fb994ef79f33b9b3017b88add22970/infrahub_testcontainers-1.6.2-py3-none-any.whl", hash = "sha256:b0608efe461681633369237aca6806a2a35eb26408a4eb30965a4f7fdf9a46c1", size = 23040, upload-time = "2025-12-22T19:18:09.481Z" }, ] [[package]] @@ -1226,7 +1209,7 @@ wheels = [ [[package]] name = "prefect-client" -version = "3.4.23" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1257,7 +1240,6 @@ dependencies = [ { name = "pydantic-settings" }, { name = "python-dateutil" }, { name = "python-slugify" }, - { name = "python-socks", extra = ["asyncio"] }, { name = "pytz" }, { name = "pyyaml" }, { name = "rfc3339-validator" }, @@ -1269,9 +1251,9 @@ dependencies = [ { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/26/f11ab9c0d1533b3b40dac593f7c5eafa4765697a9e926bdbc00687387ee4/prefect_client-3.4.23.tar.gz", hash = "sha256:23d035c1e5a9df0c69c701c5f6ad3f8b80530c19a2383b4ca319a2397fe09ac4", size = 672422, upload-time = "2025-10-09T20:39:30.556Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/ea/ca532ca405b9127b16161b7c6f6fdf44f436e9dacb8e08056080761beff7/prefect_client-3.5.0.tar.gz", hash = "sha256:10b9c700288ee1d7513fdfdb394eca0807e461f8bd8da9edf2433f276cc23a17", size = 681939, upload-time = "2025-10-31T18:03:09.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/91/3210a6924c7957073785bb3f3246ad518130dcb672b7bd12e52ae990d9f6/prefect_client-3.4.23-py3-none-any.whl", hash = "sha256:6d3ac95ced68a3d5d461e13f90df35871ce90e58194b8a354f840c6a8e6c1fa1", size = 831473, upload-time = "2025-10-09T20:39:28.377Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d3/b9ef344b5cf86dc544914d9260b657a521f6fdc97b85a591c0dfe2903371/prefect_client-3.5.0-py3-none-any.whl", hash = "sha256:70e2ab2d0e44534388e6daa052e0ee52c86b2263e0bdb450cf1101027504c191", size = 843344, upload-time = "2025-10-31T18:03:07.715Z" }, ] [[package]] @@ -1495,7 +1477,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1506,23 +1488,21 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, ] [[package]] @@ -1538,15 +1518,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/52/5c/cafa97944de55738a [[package]] name = "pytest-httpx" -version = "0.36.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/5574834da9499066fa1a5ea9c336f94dba2eae02298d36dab192fcf95c86/pytest_httpx-0.36.0.tar.gz", hash = "sha256:9edb66a5fd4388ce3c343189bc67e7e1cb50b07c2e3fc83b97d511975e8a831b", size = 56793, upload-time = "2025-12-02T16:34:57.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/08/d0be3fe5645c6cd9396093a9ddf97d60814a3b066fd5b38ddced34a13d14/pytest_httpx-0.34.0.tar.gz", hash = "sha256:3ca4b0975c0f93b985f17df19e76430c1086b5b0cce32b1af082d8901296a735", size = 54108, upload-time = "2024-11-18T18:49:56.442Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/d2/1eb1ea9c84f0d2033eb0b49675afdc71aa4ea801b74615f00f3c33b725e3/pytest_httpx-0.36.0-py3-none-any.whl", hash = "sha256:bd4c120bb80e142df856e825ec9f17981effb84d159f9fa29ed97e2357c3a9c8", size = 20229, upload-time = "2025-12-02T16:34:56.45Z" }, + { url = "https://files.pythonhosted.org/packages/2c/72/7138a0faf5d780d6b9ceedef22da0b66ae8e22a676a12fd55a05c0cdd979/pytest_httpx-0.34.0-py3-none-any.whl", hash = "sha256:42cf0a66f7b71b9111db2897e8b38a903abd33a27b11c48aff4a3c7650313af2", size = 19440, upload-time = "2024-11-18T18:49:55.384Z" }, ] [[package]] @@ -1582,20 +1562,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] -[[package]] -name = "python-socks" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/07/cfdd6a846ac859e513b4e68bb6c669a90a74d89d8d405516fba7fc9c6f0c/python_socks-2.8.0.tar.gz", hash = "sha256:340f82778b20a290bdd538ee47492978d603dff7826aaf2ce362d21ad9ee6f1b", size = 273130, upload-time = "2025-12-09T12:17:05.433Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/10/e2b575faa32d1d32e5e6041fc64794fa9f09526852a06b25353b66f52cae/python_socks-2.8.0-py3-none-any.whl", hash = "sha256:57c24b416569ccea493a101d38b0c82ed54be603aa50b6afbe64c46e4a4e4315", size = 55075, upload-time = "2025-12-09T12:17:03.269Z" }, -] - -[package.optional-dependencies] -asyncio = [ - { name = "async-timeout", marker = "python_full_version < '3.11'" }, -] - [[package]] name = "pytokens" version = "0.3.0"