diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa4cdd9..3b60c33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,41 @@ on: - 'src/codesphere/**' - '.github/workflows/ci.yml' - 'tests/**' + - 'pyproject.toml' + - 'ruff.toml' + - 'uv.lock' permissions: contents: write pull-requests: write jobs: + typecheck: + name: Type Check (ty) + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv package manager + uses: astral-sh/setup-uv@v6 + with: + activate-environment: true + + - name: Install dependencies + run: uv sync --extra dev + shell: bash + + - name: Run ty type check + run: uv run ty check + shell: bash + + - name: Minimize uv cache + run: uv cache prune --ci + security_check: name: Security Check (Bandit) runs-on: ubuntu-latest @@ -123,7 +152,7 @@ jobs: - name: Run tests with pytest run: | - uv run pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov=. --ignore=tests/integration | tee pytest-coverage.txt + uv run pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov --ignore=tests/integration | tee pytest-coverage.txt shell: bash - name: Pytest coverage comment diff --git a/.gitignore b/.gitignore index 443a516..66b724c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,8 @@ env/ .pytest_cache -__marimo__ \ No newline at end of file +__marimo__ +# Coverage artifacts +.coverage +coverage.xml +test-results/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b6152ac..d0143ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,3 +11,12 @@ repos: - id: ruff-check args: [ --fix ] - id: ruff-format + +- repo: local + hooks: + - id: ty-check + name: ty check + entry: uv run ty check + language: system + types: [python] + pass_filenames: false diff --git a/Makefile b/Makefile index fcddb07..1c41946 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install lint format test test-integration test-unit bump release pypi tree version changelog +.PHONY: help install lint format typecheck test test-integration test-unit bump release pypi tree version changelog .DEFAULT_GOAL := help @@ -27,6 +27,10 @@ format: ## Formats code with ruff @echo ">>> Formatting code with ruff..." uv run ruff format src +typecheck: ## Checks types with ty + @echo ">>> Checking types with ty..." + uv run ty check + test: ## Runs all tests with pytest @echo ">>> Running all tests with pytest..." uv run pytest diff --git a/examples/dashboard/app.py b/examples/dashboard/app.py index 21db2f9..fbd9787 100644 --- a/examples/dashboard/app.py +++ b/examples/dashboard/app.py @@ -17,9 +17,8 @@ import asyncio from contextlib import asynccontextmanager -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path -from typing import Optional from dotenv import load_dotenv from fastapi import FastAPI, Form, HTTPException, Query, Request @@ -44,7 +43,7 @@ from codesphere.resources.workspace.logs import LogStage # noqa: E402 # Global SDK instance (managed via lifespan) -cs: Optional[CodesphereSDK] = None +cs: CodesphereSDK | None = None @asynccontextmanager @@ -71,7 +70,7 @@ async def lifespan(app: FastAPI): # ============================================================================= -def format_datetime(value: Optional[datetime]) -> str: +def format_datetime(value: datetime | None) -> str: """Format datetime for display.""" if value is None: return "N/A" @@ -156,7 +155,7 @@ async def team_detail(request: Request, team_id: int): # Get usage summary (last 7 days) try: - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=7) usage = await team.usage.get_landscape_summary( begin_date=begin_date, end_date=end_date, limit=10 @@ -243,8 +242,8 @@ async def create_workspace( team_id: int = Form(...), name: str = Form(...), plan_id: int = Form(...), - base_image: Optional[str] = Form(None), - git_url: Optional[str] = Form(None), + base_image: str | None = Form(None), + git_url: str | None = Form(None), ): """Create a new workspace.""" try: @@ -265,16 +264,16 @@ async def create_workspace( "partials/error.html", {"request": request, "error": str(e)}, ) - raise HTTPException(status_code=400, detail=str(e)) + raise HTTPException(status_code=400, detail=str(e)) from e @app.post("/workspaces/{workspace_id}/update", response_class=HTMLResponse) async def update_workspace( request: Request, workspace_id: int, - name: Optional[str] = Form(None), - plan_id: Optional[int] = Form(None), - replicas: Optional[int] = Form(None), + name: str | None = Form(None), + plan_id: int | None = Form(None), + replicas: int | None = Form(None), ): """Update workspace settings.""" workspace = await cs.workspaces.get(workspace_id=workspace_id) @@ -630,8 +629,8 @@ async def logs_partial( workspace_id: int, stage: str = Query(default="prepare"), step: int = Query(default=0), - server: Optional[str] = Query(default=None), - replica: Optional[int] = Query(default=None), + server: str | None = Query(default=None), + replica: int | None = Query(default=None), ): """Get logs for a pipeline stage or server.""" workspace = await cs.workspaces.get(workspace_id=workspace_id) diff --git a/examples/scripts/create_workspace_with_landscape.py b/examples/scripts/create_workspace_with_landscape.py index 1481658..c2bab65 100644 --- a/examples/scripts/create_workspace_with_landscape.py +++ b/examples/scripts/create_workspace_with_landscape.py @@ -1,6 +1,6 @@ import asyncio import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from codesphere import CodesphereSDK from codesphere.resources.workspace import WorkspaceCreate @@ -44,7 +44,8 @@ async def main(): .add_reactive_service("web") .plan(plan.id) .add_step( - 'for i in $(seq 1 20); do echo "[$i] Processing request..."; sleep 1; done' + "for i in $(seq 1 20); do " + 'echo "[$i] Processing request..."; sleep 1; done' ) .add_port(3000, public=True) .add_path("/", port=3000) @@ -95,11 +96,12 @@ async def main(): print("\n--- Usage History ---") - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=1) print( - f"Fetching usage summary from {begin_date.isoformat()} to {end_date.isoformat()}..." + f"Fetching usage summary from {begin_date.isoformat()} " + f"to {end_date.isoformat()}..." ) usage_summary = await team.usage.get_landscape_summary( begin_date=begin_date, @@ -135,7 +137,8 @@ async def main(): print(f"Total events: {events.total_items}") for event in events.items: print( - f" [{event.date.isoformat()}] {event.action.value.upper()} by {event.initiator_email}" + f" [{event.date.isoformat()}] {event.action.value.upper()} " + f"by {event.initiator_email}" ) print("\nRefreshing usage summary...") diff --git a/examples/sdk_demo.py b/examples/sdk_demo.py index 3e9eb8a..fd4262c 100644 --- a/examples/sdk_demo.py +++ b/examples/sdk_demo.py @@ -14,6 +14,7 @@ with app.setup: """Import dependencies and initialize the SDK.""" import os + from codesphere import CodesphereSDK # Display token status diff --git a/plans/refactor/01-typing-gate-and-tooling.md b/plans/refactor/01-typing-gate-and-tooling.md new file mode 100644 index 0000000..850e659 --- /dev/null +++ b/plans/refactor/01-typing-gate-and-tooling.md @@ -0,0 +1,80 @@ +# 01 — Add a blocking `ty` type-check gate (ratcheted) and clean up tooling config + +**Priority:** P1 +**Depends on:** — +**Unblocks:** 02–07 (the ratchet is how later tickets prove their typing work) + +## Problem + +The SDK ships `src/codesphere/py.typed` (so downstream type checkers trust our annotations) and +`.github/copilot-instructions.md` states "Strict type hints required" — but **no type checker +exists anywhere**: not in dev dependencies, not in `.pre-commit-config.yaml`, not in +`.github/workflows/ci.yml`. The claim is unenforced, and several annotations in `core/` are +actively wrong (see ticket 02). + +Surrounding tooling config has drifted: + +- `ruff.toml` selects only `["F", "E4", "E7", "E9"]` and ignores `E721`/`F841` (unused locals + pass lint). Its `exclude` list references `src/codesphere_sdk/...` paths that don't exist — + leftovers from a generated-client era. +- `pyproject.toml` `[tool.coverage.run]` has `source = ["api", "handler", "tasks"]` + (pyproject.toml:64) — none of those packages exist, so coverage numbers are unanchored. + `omit` also lists nonexistent `docs/*` / `scripts/**` roots. +- `pyproject.toml:13-14,22` declare `aiohttp`, `aiohttp-retry`, and `urllib3` as runtime + dependencies; `grep -r` over `src/` shows zero imports of any of them (the SDK is pure httpx). + They bloat installs and imply a retry feature that doesn't exist (see ticket 07). +- `requires-python = ">=3.12.9"` (pyproject.toml:11) pins an oddly specific patch release, + excluding 3.12.0–3.12.8 users for no identified reason. + +## Approach + +Toolchain choice: **`ty`** (Astral) rather than mypy/pyright, to keep type checking and linting +in the same toolchain family as ruff. ty is pre-1.0: **pin its version** in the dev group and +expect occasional rule renames on upgrades (note this in a comment next to the pin). + +1. **Add ty as a blocking gate with a ratchet.** + - `uv add --dev ty` (pinned, e.g. `ty==0.0.x`). + - Configure in `pyproject.toml` under `[tool.ty]`: `src.root = "src"`, error-level rules for + the strictness-relevant checks (unresolved attributes, invalid assignments, invalid + argument types, missing/implicit `Any` where ty supports it). + - **Ratchet:** exclude the paths that cannot pass until later tickets land, via + `[[tool.ty.overrides]]` (or `src.exclude` if the pinned ty version's override granularity + is insufficient): + - `src/codesphere/core/**` and `src/codesphere/resources/**` → removed by tickets 02/04 + - `src/codesphere/http_client.py`, `src/codesphere/client.py`, `src/codesphere/config.py` → removed by ticket 03 + Annotate each exclusion with the ticket number that deletes it. + - CI: add a `typecheck` job to `.github/workflows/ci.yml` running `uv run ty check` + (blocking). Add the same to `.pre-commit-config.yaml` and a `make typecheck` target. + +2. **Broaden ruff.** + - `select = ["F", "E", "W", "I", "UP", "B", "SIM", "RUF"]`; drop the `F841` and `E721` + ignores; delete the entire stale `src/codesphere_sdk/*` exclude block. + - Run `ruff check --fix` + `ruff format`; fix the residue by hand (expect mostly import + sorting and pyupgrade rewrites like `Optional[X]` → `X | None`). + +3. **Fix coverage config.** `[tool.coverage.run] source = ["src/codesphere"]`; prune the + nonexistent `omit` entries; change the CI pytest invocation from `--cov=.` to rely on the + config (`--cov`). + +4. **Prune dependencies.** Remove `aiohttp`, `aiohttp-retry`, `urllib3` from + `[project.dependencies]`. While there, grep-verify each remaining runtime dep is actually + imported (`python-dateutil` is suspect) and remove any that aren't. `uv lock` after. + +5. **Relax the Python floor** to `requires-python = ">=3.12"` (align `.python-version` and + `ruff.toml target-version = "py312"`, which already agree). + +## Breaking changes + +None. Dependency removals only shrink the install footprint; relaxing `requires-python` widens +compatibility. + +## Acceptance criteria + +- [ ] CI fails on any ty error outside the documented ratchet exclusions; `make typecheck` and + the pre-commit hook run the same command. +- [ ] Every ratchet exclusion carries a comment naming the ticket that removes it. +- [ ] `ruff check` passes with the broadened rule set; no `codesphere_sdk` references remain in + `ruff.toml`. +- [ ] Coverage output reports lines in `src/codesphere/**` (spot-check the CI coverage comment). +- [ ] Fresh `uv pip install .` pulls neither aiohttp nor urllib3; `uv.lock` regenerated. +- [ ] `requires-python = ">=3.12"`. diff --git a/plans/refactor/02-typed-operation-dispatch.md b/plans/refactor/02-typed-operation-dispatch.md new file mode 100644 index 0000000..8043d48 --- /dev/null +++ b/plans/refactor/02-typed-operation-dispatch.md @@ -0,0 +1,215 @@ +# 02 — Replace `__getattribute__` operation magic with an explicit, fully-typed `_execute` + +**Priority:** P1 — keystone ticket +**Depends on:** 01 (ty ratchet in place) +**Unblocks:** 04, 05, 06 + +## Problem + +The operation dispatch layer is invisible to type checkers and its declared types are wrong in +four independent ways: + +1. **The `AsyncCallable` alias lies about its signature.** + `src/codesphere/core/operations.py:10`: + ```python + AsyncCallable: TypeAlias = Callable[[], Awaitable[_T]] + ``` + declares **zero parameters**, but every call site passes kwargs — + `self.get_team_op(team_id=team_id)` (`resources/team/resources.py:28`), + `self.create_team_op(data=payload)` (`:33`), and the same in every other resource. Under any + type checker each operation call is a "too many arguments" error. + +2. **The wiring is runtime magic no checker can follow.** Resources declare ops as + `op: AsyncCallable[T] = Field(default=_OP, exclude=True)` on classes that are **not** + pydantic models (`ResourceBase` in `core/base.py:13` is a plain class), so `Field(...)` is + just a `FieldInfo` in a class attribute and `exclude=True` is meaningless. + `_APIOperationExecutor.__getattribute__` (`core/handler.py:18-31`) then sniffs **every** + attribute access for `FieldInfo`/`APIOperation` and swaps in + `partial(self._execute_operation, operation=op)`. The static type (`AsyncCallable`), the + assigned value (`FieldInfo`), and the runtime value (a partial) are three different things. + +3. **The executor spine is `Any` end to end.** `_execute_operation` (`handler.py:33`), + `execute` (`:54`), and `_parse_and_validate_response` (`:106`) all return `Any`; + `APIOperation.response_model: Type[ResponseT]` (`operations.py:18`) can't hold `type(None)` + under strict checking; **`input_model` (`operations.py:19`) is declared on every operation + but never read by the handler** — request payloads are never validated; and the + `get_origin(response_model) is list` branch (`handler.py:123-124`) is dead because real list + responses are `ResourceList[...]` RootModels (`get_origin` returns `ResourceList`, not + `list`). + +4. **Endpoint formatting scrapes hidden state and has a payload bug.** + `_prepare_request_args` (`handler.py:65-75`) pours `self.executor.__dict__` and a full + `model_dump()` into the format namespace (duplicating `format_args.update(self.kwargs)` at + lines 67 and 73), so which value fills `{workspace_id}` is not grep-able. And + `if json_data_obj := self.kwargs.get("data")` (`handler.py:78`) drops **falsy** payloads: an + empty-dict body is silently not sent. + +Related idiom smell fixed by the same rewrite: entity models mix pydantic and executor and use +a doubled annotation+assignment (`team/schemas.py:31-32`): +```python +delete: AsyncCallable[None] +delete = Field(default=APIOperation(...), exclude=True) +``` + +## Approach + +**Chosen design: explicit generic `_execute` (delete the attribute magic).** A descriptor-based +alternative (`APIOperation.__get__` returning a typed callable) was considered and rejected: the +per-op parameters live in a *string* (`endpoint_template="/teams/{team_id}"`), so a descriptor +can type the return value but never the parameters without hand-writing a per-op `Protocol` — +duplicating the parameter list while keeping the magic. Meanwhile every resource **already** +wraps each op in a public `async def` with the real typed signature; that wrapper is the typed +surface. Collapsing to one layer is mostly deletion, and half the codebase +(`Workspace.update/delete`, `WorkspaceLandscapeManager`, …) already calls +`self._execute_operation(_OP, ...)` directly. + +### New `core/operations.py` + +```python +from dataclasses import dataclass +from typing import Generic, TypeVar +from pydantic import BaseModel + +ResponseT = TypeVar("ResponseT") +EntryT = TypeVar("EntryT", bound=BaseModel) + +@dataclass(frozen=True, slots=True) +class APIOperation(Generic[ResponseT]): + method: str + endpoint_template: str + response_model: type[ResponseT] + input_model: type[BaseModel] | None = None # now enforced by the executor + +@dataclass(frozen=True, slots=True) +class StreamOperation(Generic[EntryT]): + endpoint_template: str + entry_model: type[EntryT] +``` + +Typing facts that make this strict-clean: pydantic parametrized generics +(`ResourceList[Team]`) are real runtime classes, so `response_model=ResourceList[Team]` infers +`APIOperation[ResourceList[Team]]`; `types.NoneType` is `type[None]`, so delete/void ops are +`APIOperation[None]` and `_execute` returns `None`. Dropping `BaseModel` as the op container +removes `arbitrary_types_allowed` and the `model_copy(update={"response_model": ...})` trick in +`team/domain/manager.py:20-53` (ticket 04 deletes those call sites; use `dataclasses.replace` +if one genuinely remains). Standardize the "no response" encoding on `NoneType` — retire the +`None`-vs-`type(None)` dual sentinel. + +### New `core/handler.py` — one free function + +```python +async def execute_operation( + client: APIHttpClient, + op: APIOperation[ResponseT], + *, + data: BaseModel | Mapping[str, Any] | None = None, + params: Mapping[str, Any] | None = None, + path_params: Mapping[str, Any], +) -> ResponseT: + # 1. endpoint = op.endpoint_template.format(**path_params) — explicit, no __dict__ scraping + # 2. if op.input_model is not None and data is not None: + # data = op.input_model.model_validate(data) # input_model finally enforced + # 3. payload = data.model_dump(by_alias=True, exclude_none=True) if isinstance(data, BaseModel) else data + # guard with `if data is not None` — fixes the falsy empty-dict bug + # 4. response = await client.request(op.method, endpoint, json=payload, params=params) + # 5. return _parse_response(response, op.response_model, client) +``` + +`_parse_response(response, model: type[ResponseT], client) -> ResponseT` handles: `NoneType` → +return `None`; otherwise `model.model_validate(response.json())` + inject the client into +bound models / `ResourceList` items (port `_inject_client_into_model`). The dead +`get_origin(...) is list` branch is not ported. Wrap the pydantic `ValidationError` in the +SDK's own response-validation error (or re-raise deliberately and document it) so users don't +have to catch pydantic internals. + +### New bases in `core/base.py` + +```python +class ResourceBase: + def __init__(self, http_client: APIHttpClient) -> None: + self._http_client = http_client + + async def _execute( + self, op: APIOperation[ResponseT], *, + data: BaseModel | Mapping[str, Any] | None = None, + params: Mapping[str, Any] | None = None, + **path_params: Any, + ) -> ResponseT: + return await execute_operation(self._http_client, op, data=data, params=params, path_params=path_params) + + +class BoundModel(CamelModel): + """API entity that carries the HTTP client it was fetched with (Team, Workspace, Domain, …).""" + _http_client: APIHttpClient | None = PrivateAttr(default=None) + + def _client(self) -> APIHttpClient: + # replaces validate_http_client(); raise the "detached model" RuntimeError (or a + # dedicated DetachedModelError) when None + ... + + async def _execute(self, op: APIOperation[ResponseT], *, data=None, params=None, **path_params: Any) -> ResponseT: + return await execute_operation(self._client(), op, data=data, params=params, path_params=path_params) +``` + +`_execute` is deliberately duplicated as a one-line delegation in each base instead of a shared +mixin — mixing a plain class into pydantic bases is what produced the current +`PrivateAttr`-on-plain-class confusion. `data`/`params` are reserved kwarg names (cannot be +path params); no current endpoint template uses either. + +**Deleted:** `_APIOperationExecutor`, `APIRequestHandler`, `AsyncCallable`, the +`__getattribute__` override. `core/__init__.py` re-exports become: `ResourceBase`, +`BoundModel`, `CamelModel`, `ResourceList`, `APIOperation`, `StreamOperation`. + +### A resource module after + +```python +# resources/team/resources.py +class TeamsResource(ResourceBase): + async def list(self) -> list[Team]: + return (await self._execute(LIST_TEAMS)).root + + async def get(self, team_id: int) -> Team: + return await self._execute(GET_TEAM, team_id=team_id) + + async def create(self, payload: TeamCreate) -> Team: + return await self._execute(CREATE_TEAM, data=payload) + +# resources/team/schemas.py — delete op moves to operations.py as DELETE_TEAM: APIOperation[None] +class Team(TeamBase, BoundModel): + async def delete(self) -> None: + await self._execute(DELETE_TEAM, team_id=self.id) +``` + +Migration is mechanical across the ~8 resource modules: delete the `Field`-wired op +attributes, keep every existing public method signature, and replace the body with one +`self._execute(...)` call. Templates that previously scraped `{id}`/`{workspace_id}` from +`self.__dict__` now receive them explicitly (`id=self.id`) — one extra kwarg, grep-able data +flow. + +**Document the contributor recipe in CONTRIBUTING.md** (this ticket): a new resource is +(1) pydantic schemas in `schemas.py` (`CamelModel`, or `BoundModel` if the entity makes calls), +(2) `APIOperation` constants in `operations.py`, (3) a `ResourceBase` subclass with one typed +public method per op. No Field wiring, no dunder knowledge. + +## Breaking changes + +- **User-facing API: none.** Public signatures already lived on the wrapper methods and are + unchanged; `sdk.teams.get(...)`, `team.delete()`, `workspace.env_vars...` behave identically. +- `codesphere.core` internals change/disappear. This ticket adds a line to the README/CHANGELOG + declaring that only top-level `codesphere` exports (and the documented + `codesphere.resources.*` re-exports) are public API. +- Behavior fix: payloads are now validated against `input_model` (previously silently + unvalidated) and empty-dict bodies are now sent (previously dropped). Both are corrections; + call them out in the CHANGELOG. + +## Acceptance criteria + +- [ ] `grep -rn "AsyncCallable\|__getattribute__\|APIRequestHandler\|_APIOperationExecutor" src/` is empty. +- [ ] `grep -rn "Field(default=" src/codesphere/resources src/codesphere/core` finds no + operation wiring. +- [ ] `src/codesphere/core/**` and `src/codesphere/resources/**` removed from the ty ratchet; + `uv run ty check` passes. +- [ ] New unit test: a payload violating `input_model` raises a validation error before any + HTTP request is made. +- [ ] New regression test: an operation called with `data={}` sends `{}` as the JSON body. +- [ ] Full unit + integration suites pass; CONTRIBUTING.md contains the new-resource recipe. diff --git a/plans/refactor/03-client-config-lifecycle.md b/plans/refactor/03-client-config-lifecycle.md new file mode 100644 index 0000000..fdd6cdc --- /dev/null +++ b/plans/refactor/03-client-config-lifecycle.md @@ -0,0 +1,92 @@ +# 03 — Constructor-injectable config, lazy settings, single close path + +**Priority:** P1 +**Depends on:** 01; mostly parallel to 02 (one noted overlap) + +## Problem + +1. **`import codesphere` crashes without `CS_TOKEN`.** `src/codesphere/config.py:20` runs + `settings = Settings()` at module import and `token: SecretStr` (`config.py:13`) is + required, so a bare import raises a raw pydantic `ValidationError`. The purpose-built + `AuthenticationError` in `exceptions.py` (whose message even says to set `CS_TOKEN`) is + **never raised anywhere** — dead code. + +2. **No per-client configuration.** `CodesphereSDK.__init__` (`client.py:12-16`) takes no + arguments and `APIHttpClient.__init__` reads the token from the global `settings` + (`http_client.py:16`). Two clients with different tokens in one process are impossible; + pointing at a staging URL requires env mutation before import. + +3. **`CS_BASE_URL` is dead.** `http_client.py:15-17`: + ```python + def __init__(self, base_url: str = "https://codesphere.com/api"): + self._base_url = base_url or str(settings.base_url) + ``` + The parameter default is a non-empty string, so `settings.base_url` is only consulted if a + caller passes `""` — and `client.py:13` calls `APIHttpClient()` with no args. The env var + and the duplicated literal can silently disagree. + +4. Assorted lifecycle/typing debris: + - `http_client.py:29-30` `setattr`s dynamic `get/post/...` verb methods — untyped, + invisible to IDEs, and unused (the handler always calls `.request`). + - Duplicate payload serialization: the handler dumps `BaseModel` payloads AND + `http_client.request` re-dumps (`http_client.py:62-63`). + - `client.py` has two divergent close paths: `close()` (`:21-22`) discards exception info + while `__aexit__` (`:28-29`) bypasses `close()` entirely. + - `config.py:14` `base_url: HttpUrl = "https://codesphere.com/api"` — a `str` default on an + `HttpUrl` field (runtime-coerced, statically wrong). + +## Approach + +1. **Make settings lazy.** Delete module-level `settings = Settings()`. Make + `token: SecretStr | None = None`. `Settings()` is constructed inside the client path only + (a small `Settings.load()` or plain construction in `CodesphereSDK.__init__`). Fix the + `base_url` default to a proper `HttpUrl` value so the field type-checks. + +2. **Constructor injection with explicit resolution order.** + ```python + class CodesphereSDK: + def __init__( + self, + token: str | SecretStr | None = None, + base_url: str | None = None, + timeout: httpx.Timeout | None = None, + http_client: APIHttpClient | None = None, # escape hatch for tests/advanced use + ) -> None: ... + ``` + Resolution: explicit argument → `CS_*` env via `Settings()` → built-in default. A missing + token raises `AuthenticationError` **at construction** (finally using the existing + exception and its message). + +3. **Make `APIHttpClient` a dumb transport.** + `def __init__(self, *, token: SecretStr, base_url: str, timeout: httpx.Timeout)` — no + `settings` access inside; the SDK resolves config, the transport receives it. Delete the + `setattr` verb loop. Remove the `BaseModel` re-dump from `request` so serialization is + owned solely by `execute_operation` (from ticket 02 — if 03 lands first, keep the re-dump + until 02 merges, then delete it there). + +4. **One close path.** `__aexit__` delegates to a single `close(exc_type=None, ...)`; + `CodesphereSDK.close()` and `__aexit__` both route through it. + +5. Update README/examples for `CodesphereSDK(token=...)` (env-only usage keeps working + unchanged). + +## Breaking changes + +- `import codesphere` no longer raises when `CS_TOKEN` is unset — the failure moves to + `CodesphereSDK()` construction and changes type from pydantic `ValidationError` to + `codesphere.AuthenticationError`. Anyone catching the pydantic error at import time (nobody + should be) is affected. CHANGELOG entry: behavior fix. +- `APIHttpClient.__init__` signature changes (internal; ticket 02 declares `core`/transport + internals private). + +## Acceptance criteria + +- [ ] `env -i python -c "import codesphere"` succeeds (no env vars needed to import). +- [ ] `CodesphereSDK()` without a token (arg or env) raises `AuthenticationError`. +- [ ] `CodesphereSDK(token="x", base_url="http://localhost:8000")` sends requests to the + custom URL (respx test); `CS_BASE_URL` env var is honored when no arg is given (test). +- [ ] Two SDK instances with different tokens send different `Authorization` headers (test). +- [ ] No dynamic `setattr` verb methods; exactly one payload-serialization site; exactly one + close path (`grep` + tests). +- [ ] `config.py`, `http_client.py`, `client.py` removed from the ty ratchet; `uv run ty check` + passes. diff --git a/plans/refactor/04-resource-layer-normalization.md b/plans/refactor/04-resource-layer-normalization.md new file mode 100644 index 0000000..0aef2a9 --- /dev/null +++ b/plans/refactor/04-resource-layer-normalization.md @@ -0,0 +1,90 @@ +# 04 — One module layout, one execution idiom, shared scoped-resource bases + +**Priority:** P2 +**Depends on:** 02 + +## Problem + +The resource packages never settled on a single pattern, so a contributor has no canonical +template to copy: + +- **File naming chaos.** The manager class lives in `manager.py` for `team/domain` and + `team/usage`, but in `models.py` for `workspace/envVars`, `workspace/git`, + `workspace/landscape`, `workspace/logs`. `git` uses `schema.py` (singular) while everything + else uses `schemas.py`. The `envVars/` directory is camelCase in a snake_case tree. + `workspace/landscape/resources.py` is a zero-byte file; `resources/__init__.py` is empty. +- **Two contradictory execution idioms** (pre-ticket-02): Field-wired ops vs direct + `self._execute_operation(_OP, ...)` calls. Ticket 02 standardizes the mechanism; this ticket + sweeps the remaining structural drift. +- **Copy-pasted scoped-manager boilerplate.** Six managers repeat the same + `__init__(self, http_client, workspace_id/team_id)` (e.g. `envVars/models.py`, + `git/models.py`, `landscape/models.py`, `logs/models.py`, `domain/manager.py`, + `usage/manager.py`). +- **Domain ops defined twice.** `team/domain/operations.py` sets `response_model=DomainBase`, + then `team/domain/manager.py:20-53` overrides every single op via + `model_copy(update={"response_model": Domain})`. `Domain.verify_status` is annotated + `AsyncCallable[None]` while its op returns `DomainVerificationStatus`. +- **Convention breaks & duplication.** `EnvVar` (`envVars/schemas.py`) uses plain `BaseModel` + instead of `CamelModel` (loses aliasing and the `to_dict/to_json/to_yaml` helpers). + `PipelineStatusList` (`landscape/schemas.py:41`) reimplements exactly what + `core.base.ResourceList` provides. The `min(max(1, x), 100)` clamp appears 4× in + `team/usage/manager.py:72,91,128,151`. `UsageSummaryResponse.refresh` and + `UsageEventsResponse.refresh` are near-identical. `utils.dict_to_model_list` has zero callers + and a `key_field: str = None` typing bug. + +## Approach + +1. **Canonical per-resource layout** (document in CONTRIBUTING.md alongside ticket 02's + recipe): each resource package contains exactly `operations.py` (op constants), + `schemas.py` (pydantic models), `resources.py` (`ResourceBase` subclasses), `__init__.py` + (re-exports). Rename `manager.py`/`models.py`/`schema.py` accordingly; delete the + zero-byte `landscape/resources.py` placeholder content merge. + +2. **Rename `envVars/` → `env_vars/`**, keeping the public import surface stable: the + re-exports in `workspace/__init__.py` (`from .env_vars import EnvVar, ...`) keep the paths + users actually import (`codesphere.resources.workspace.envVars` deep imports appear in + examples — keep a thin `envVars.py` shim module emitting `DeprecationWarning`, or update + the examples and CHANGELOG if a clean break is preferred; decide at implementation time, + shim recommended since the SDK is published). + +3. **Scoped bases in `core/base.py`:** + ```python + class TeamScopedResource(ResourceBase): + def __init__(self, http_client: APIHttpClient, team_id: int) -> None: ... + + class WorkspaceScopedResource(ResourceBase): + def __init__(self, http_client: APIHttpClient, workspace_id: int) -> None: ... + ``` + All six scoped managers subclass these; the copy-pasted `__init__`s are deleted. + +4. **Domain ops defined once** with their final `response_model=Domain` / + `ResourceList[Domain]` directly in `operations.py` (ticket 02's frozen dataclass makes + `model_copy` impossible anyway). Fix `verify_status` to return + `DomainVerificationStatus`, matching its op. + +5. **Convention/duplication sweep:** + - `EnvVar(CamelModel)`. + - Delete `PipelineStatusList`; use `ResourceList[PipelineStatus]` (re-export the old name + as an alias for compatibility). + - One clamp: `PageSize = Annotated[int, Field(ge=1, le=100)]` on the request models, or a + single `_clamp_page_size()` helper in `team/usage`. + - Hoist the shared `refresh()` onto `PaginatedResponse`; while there, make + `limit`/`offset` non-Optional with defaults (removes the repeated `self.limit or 25` + re-defaulting). + - Delete `utils.dict_to_model_list` (dead code); if `utils.py` becomes empty, delete it. + +## Breaking changes + +- Internal module paths move (`...domain.manager` → `...domain.resources`, etc.). Public + re-exports from `codesphere` and the package `__init__.py`s stay stable; the `envVars` deep + path gets a deprecation shim (recommended) per step 2. + +## Acceptance criteria + +- [ ] `find src -name "manager.py" -o -name "models.py" -o -name "schema.py"` returns nothing; + no zero-byte modules; no camelCase directories (modulo the optional shim module). +- [ ] `grep -rn "model_copy(update" src/codesphere/resources` is empty. +- [ ] At most one page-size clamp implementation; one `refresh()` implementation. +- [ ] `EnvVar` round-trips camelCase aliases like every other schema (test). +- [ ] Examples still run (`examples/sdk_demo.py` imports resolve). +- [ ] Touched paths removed from the ty ratchet; `uv run ty check` passes. diff --git a/plans/refactor/05-logs-landscape-hardening.md b/plans/refactor/05-logs-landscape-hardening.md new file mode 100644 index 0000000..6cdbbea --- /dev/null +++ b/plans/refactor/05-logs-landscape-hardening.md @@ -0,0 +1,84 @@ +# 05 — Deduplicate the logs stream/collect trio; make profile writes injection-safe + +**Priority:** P2 +**Depends on:** 02 + +## Problem + +1. **The stream/collect API is the same code six times.** `workspace/logs/models.py` defines + `stream` / `stream_server` / `stream_replica` (lines ~205-237) and `collect` / + `collect_server` / `collect_replica` (~238-293). Each trio differs only in which + `StreamOperation` it selects and which id kwarg it passes; each `collect_*` is an identical + `async with ... as stream: async for ...` accumulation wrapper around its `stream_*`. Any + fix or feature (filters, timeouts) must be applied six times. + +2. **`save_profile` builds a shell heredoc from user content.** + `workspace/landscape/models.py:87`: + ```python + f"cat > {filename} << 'PROFILE_EOF'\n{body}PROFILE_EOF\n" + ``` + executed remotely via `execute_command`. The profile *name* is regex-validated, but the + YAML *body* is interpolated raw: content containing a line `PROFILE_EOF` truncates the file + and executes the remainder as shell; CRLF/backslash edge cases corrupt content. + `delete_profile` (`models.py:95`) does `rm -f {filename}` — safe only because of the name + regex, worth an assertion. This is also a design smell (remote file management expressed as + string-built shell), but the injection fix is the actionable part. + +3. Minor: `LogStream` hardcodes `httpx.Timeout(5.0, read=None)` and SSE terminator event names + (`"end", "close", "done", "complete"`), and reaches into the private + `self._http_client._get_client()`. + +## Approach + +1. **One parametrized stream path.** Introduce frozen target dataclasses, each knowing its + `StreamOperation` and path params: + ```python + @dataclass(frozen=True, slots=True) + class StageTarget: stage: LogStage; step: int = 0 + @dataclass(frozen=True, slots=True) + class ServerTarget: step: int; server: str + @dataclass(frozen=True, slots=True) + class ReplicaTarget: step: int; replica: str + + LogTarget = StageTarget | ServerTarget | ReplicaTarget + + class WorkspaceLogsResource(WorkspaceScopedResource): + def stream(self, target: LogTarget, ...) -> LogStream: ... + async def collect(self, target: LogTarget, ...) -> list[LogEntry]: # implemented on stream() + ``` + Keep the six existing method names as one-line deprecated shims delegating to + `stream(...)`/`collect(...)` (emit `DeprecationWarning`), since the SDK is published. + +2. **Base64 transport for profile content.** Replace the heredoc with content-independent + plumbing: + ```python + f"printf '%s' '{b64}' | base64 -d > '{filename}'" + ``` + where `b64 = base64.b64encode(body.encode()).decode()` (alphabet is `[A-Za-z0-9+/=]`, inert + in single quotes) and `filename` remains regex-validated (`ci..yml`). Add an + adversarial round-trip unit test: content containing `PROFILE_EOF`, single/double quotes, + `$(reboot)`, backticks, CRLF, and unicode must be written byte-identically (assert on the + command string in unit tests; optionally cover in integration). + +3. While in the file: take the SSE read timeout from the client's configured timeouts instead + of the hardcoded `Timeout(5.0, read=None)`; name the terminator event set as a module-level + constant; expose whatever accessor `LogStream` needs on `APIHttpClient` instead of calling + `_get_client()` from outside. + +4. Tighten `LogEntry` typing: `kind` becomes a `str`-based `Enum` (`"I"`/`"E"`, with a + fallback for unknown values) instead of `Optional[str]` on an `extra="allow"` model. + +## Breaking changes + +None — old method names remain as deprecated shims; profile files are byte-identical after the +transport change. + +## Acceptance criteria + +- [ ] Stream/collect logic exists once; the six legacy names are 1-line shims with + `DeprecationWarning` (tested via `pytest.warns`). +- [ ] Adversarial-content profile test passes (including a literal `PROFILE_EOF` line and + `$(...)`). +- [ ] No heredoc construction remains: `grep -rn "PROFILE_EOF" src/` is empty. +- [ ] `workspace/logs/**` and `workspace/landscape/**` removed from the ty ratchet; + `uv run ty check` passes. diff --git a/plans/refactor/06-test-suite-http-boundary.md b/plans/refactor/06-test-suite-http-boundary.md new file mode 100644 index 0000000..fadd42a --- /dev/null +++ b/plans/refactor/06-test-suite-http-boundary.md @@ -0,0 +1,58 @@ +# 06 — Test at the HTTP boundary (respx); cover the untested core modules + +**Priority:** P2 +**Depends on:** 02, 03 + +## Problem + +- Unit tests mock at the dispatch boundary — e.g. + `landscape_manager._execute_operation = AsyncMock()` + (`tests/resources/workspace/landscape/test_pipeline.py:94`) — so real request construction, + URL/path formatting, auth header injection, error mapping, and response deserialization are + never exercised by unit tests. Only the live-credential integration suite (gated to the + upstream repo's secrets) touches that path. +- **Zero dedicated unit tests** exist for `exceptions.py` (10 exported error types, including + the `raise_for_status` status→exception mapping and `Retry-After` parsing), `config.py`, + `http_client.py`, and `utils.py`. + +## Approach + +1. Add `respx` to the dev group. Shared fixture (e.g. in `tests/conftest.py`): + `mock_api` — a `respx.mock` router bound to the SDK's base URL, plus an `sdk` fixture + yielding an opened `CodesphereSDK(token="test-token", base_url="https://test.local/api")` + (uses ticket 03's constructor injection). + +2. **Exception matrix** (`tests/test_exceptions.py`): for each mapped status + (400, 401, 403, 404, 409, 422, 429, 5xx) assert the raised class, `message`, `status_code`, + parsed `errors`, and `RateLimitError.retry_after` from the `Retry-After` header; plus + non-JSON error bodies and the httpx timeout/connect-error → `TimeoutError`/`NetworkError` + mapping in `http_client.request`. + +3. **Executor behavior** (`tests/core/test_execute_operation.py`), asserting on the *real* + outgoing request via respx: endpoint template formatting from explicit path params + (missing param → clear error), `input_model` validation rejecting bad payloads before any + request, `data={}` sending an empty JSON object (falsy-body regression from ticket 02), + `APIOperation[None]` returning `None` without parsing, `ResourceList` items receiving the + injected client (calling a method on a returned entity hits the mock). + +4. **Config resolution** (`tests/test_config.py`): arg > env > default precedence for token + and base_url; import without env vars; `AuthenticationError` on missing token; two SDKs → + two Authorization headers (from ticket 03's criteria). + +5. **Migration policy:** new tests are respx-only; existing `AsyncMock(_execute...)` tests are + converted opportunistically whenever their module is touched by tickets 02/04/05 — no + big-bang rewrite. Add a short note to CONTRIBUTING.md: unit tests for request/response + behavior mock the transport (respx), not SDK internals. + +## Breaking changes + +None (tests and dev dependencies only). + +## Acceptance criteria + +- [ ] Line coverage > 90% for `exceptions.py`, `config.py`, `http_client.py`, and the executor + module (measured with ticket 01's fixed coverage config). +- [ ] No **new** test mocks `_execute`/`_execute_operation` or SDK-internal methods for + request/response behavior. +- [ ] The exception matrix covers every exception class exported from `codesphere.__init__`. +- [ ] Suite runs offline (no `CS_*` env needed) and passes in CI. diff --git a/plans/refactor/07-retry-support.md b/plans/refactor/07-retry-support.md new file mode 100644 index 0000000..80da84e --- /dev/null +++ b/plans/refactor/07-retry-support.md @@ -0,0 +1,53 @@ +# 07 — Opt-in retry with backoff, honoring `Retry-After` + +**Priority:** P3 +**Depends on:** 03 (constructor injection), 06 (respx harness for tests) + +## Problem + +The SDK parses `Retry-After` into `RateLimitError.retry_after` (`exceptions.py`), and until +ticket 01 it even shipped `aiohttp-retry` as a dependency — but **no retry logic exists +anywhere**. Nothing consumes `retry_after`; a 429 or a transient 503 always surfaces +immediately to the caller. The feature is implied but absent. + +## Approach + +1. **Config object** (in `config.py` or a new `retry.py`): + ```python + @dataclass(frozen=True, slots=True) + class RetryConfig: + max_retries: int = 0 # 0 = disabled (default: zero behavior change) + backoff_factor: float = 0.5 # sleep = backoff_factor * 2**attempt, jittered + retry_statuses: frozenset[int] = frozenset({429, 502, 503, 504}) + retry_methods: frozenset[str] = frozenset({"GET", "HEAD", "PUT", "DELETE"}) # idempotent only + ``` + +2. **Wire-through:** `CodesphereSDK(..., retry: RetryConfig | None = None)` → passed to + `APIHttpClient`. + +3. **Implementation:** a loop in `APIHttpClient.request` around the send + `raise_for_status`: + - retry on a response whose status is in `retry_statuses` **and** whose method is in + `retry_methods` (POST retried only if the caller opts in via `retry_methods`), and on + `httpx.TimeoutException`/`httpx.ConnectError`; + - sleep = `Retry-After` header value when present (seconds or HTTP-date), else exponential + backoff with jitter, via `asyncio.sleep`; + - after `max_retries` attempts, raise the same exception the non-retry path would + (`RateLimitError`, `NetworkError`, …) so error handling is unchanged; + - `log.debug` each retry with attempt count and delay. + + Keep it a plain loop — do not reintroduce a retry dependency; httpx has no native retry and + the logic is ~30 lines. + +## Breaking changes + +None. Default `max_retries=0` means behavior is identical unless a user opts in. + +## Acceptance criteria + +- [ ] respx test: 429 with `Retry-After: 1` then 200 → succeeds, exactly 2 requests, waited + ≥1s (patch/measure sleep). +- [ ] respx test: retries exhausted → the original `RateLimitError` is raised with + `retry_after` populated. +- [ ] respx test: non-idempotent POST is **not** retried by default. +- [ ] Default construction performs zero retries (single request on failure). +- [ ] README documents `RetryConfig` with an example. diff --git a/plans/refactor/README.md b/plans/refactor/README.md new file mode 100644 index 0000000..39b645d --- /dev/null +++ b/plans/refactor/README.md @@ -0,0 +1,42 @@ +# Refactor Tickets + +Outcome of a full codebase review (2026-07-10) focused on **strong typing** and **easy extensibility**. + +The SDK's layered design (declarative `APIOperation` constants + thin resource facades + pydantic +response validation) is a solid, extensible spine. The findings concentrate on two gaps: + +1. **The typing story is aspirational, not enforced.** The package ships `py.typed` and the + contribution docs demand strict type hints, but no type checker runs anywhere — and the core + dispatch mechanism (`__getattribute__` interception of `Field`-wrapped operations) could not + pass one as written. +2. **The declared contracts aren't what the runtime does.** `input_model` is never validated, + `CS_BASE_URL` is dead, `import codesphere` crashes without `CS_TOKEN`, and empty request + bodies are silently dropped. + +## Tickets + +| # | Ticket | Priority | Depends on | +|---|--------|----------|------------| +| 01 | [Typing gate (ty) & tooling cleanup](01-typing-gate-and-tooling.md) | P1 | — | +| 02 | [Typed operation dispatch](02-typed-operation-dispatch.md) | P1 (keystone) | 01 | +| 03 | [Client config & lifecycle](03-client-config-lifecycle.md) | P1 | 01 (partially parallel to 02) | +| 04 | [Resource layer normalization](04-resource-layer-normalization.md) | P2 | 02 | +| 05 | [Logs & landscape hardening](05-logs-landscape-hardening.md) | P2 | 02 | +| 06 | [Test suite at the HTTP boundary](06-test-suite-http-boundary.md) | P2 | 02, 03 | +| 07 | [Opt-in retry support](07-retry-support.md) | P3 | 03, 06 | + +## Sequencing rationale + +- **01 lands first** and turns the `ty` gate on immediately as a *ratchet*: known-broken paths + (`core/`, `resources/`, `http_client.py`, `client.py`) are excluded at first, so the gate is + blocking for everything else (and all new code) from day one without waiting on the dispatch + rewrite. +- **02 is the keystone** — every later ticket builds on the explicit `_execute` dispatch it + introduces. Each ticket's acceptance criteria include removing its own paths from the ty + ratchet, so type-checked coverage only ever grows. +- **03** can start in parallel with 02 (different files), with one small ordering note on the + duplicate-serialization removal (see ticket). +- **Public API stability:** across all tickets, only symbols exported from the top-level + `codesphere` package (plus documented deep imports like `codesphere.resources.workspace.*` + re-exports) are treated as public. `codesphere.core` internals may change freely; ticket 02 + formalizes this. diff --git a/pyproject.toml b/pyproject.toml index 6e8d0b0..0f14536 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,18 +8,13 @@ license = { file="LICENSE" } authors = [ { name = "Datata1", email = "jan-david.wiederstein@codesphere.com" } ] -requires-python = ">=3.12.9" +requires-python = ">=3.12" dependencies = [ - "aiohttp>=3.12.13", - "aiohttp-retry>=2.9.1", "httpx>=0.28.1", "pydantic>=2.11.7", "pydantic-settings>=2.11.0", - "python-dateutil>=2.9.0.post0", "python-dotenv>=1.2.1", "pyyaml>=6.0.2", - "typing-extensions>=4.14.0", - "urllib3>=2.4.0", ] classifiers = [ "Programming Language :: Python :: 3", @@ -45,6 +40,7 @@ dev = [ "pytest-cov>=6.2.1", "ruff>=0.11.13", "bandit>=1.7.0", + "ty==0.0.58", ] [tool.pytest.ini_options] @@ -61,18 +57,10 @@ markers = [ ] [tool.coverage.run] -source = ["api", "handler", "tasks"] +source = ["src/codesphere"] branch = true omit = [ - ".github/*", - ".ruff_cache/*", - "docs/*", - "examples/**", - "scripts/**", "*/__main__.py", - "tests/*", - ".venv/*", - "__init__.py" ] [tool.coverage.report] @@ -95,6 +83,23 @@ targets = ["src/codesphere"] exclude_dirs = ["tests", ".venv", ".uv", "examples", "docs", ".github", "scripts"] skips = ["B101"] +[tool.ty.src] +exclude = [ + # --- Type-check ratchet ------------------------------------------------- + # Each exclusion below is temporary and owned by a refactor ticket in + # plans/refactor/. When a ticket lands, its paths MUST be removed here. + # Do not add new exclusions without a ticket reference. + "src/codesphere/core/", # ticket 02 (typed operation dispatch) + "src/codesphere/resources/", # tickets 02 + 04 (dispatch, normalization) + "src/codesphere/http_client.py", # ticket 03 (client config & lifecycle) + "src/codesphere/config.py", # ticket 03 (client config & lifecycle) + "tests/", # ticket 06 (respx-based test suite) + # --- Permanent exclusions ----------------------------------------------- + # Examples have their own dependencies (e.g. fastapi) not installed in + # this project's environment. + "examples/", +] + [project.urls] Homepage = "https://codesphere.com/" Repository = "https://github.com/Datata1/codesphere-python" diff --git a/ruff.toml b/ruff.toml index f522bc3..bda7c30 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,27 +4,16 @@ indent-width = 4 target-version = "py312" -[lint] -select = ["F", "E4", "E7", "E9"] -ignore = ["E721", "F841"] - - exclude = [ ".git", ".venv", "build", "dist", - "src/codesphere_sdk/api", - "src/codesphere_sdk/models", - "src/codesphere_sdk/api_client.py", - "src/codesphere_sdk/api_response.py", - "src/codesphere_sdk/configuration.py", - "src/codesphere_sdk/exceptions.py", - "src/codesphere_sdk/rest.py", - "src/codesphere_sdk/docs", - "src/codesphere_sdk/test" ] +[lint] +select = ["F", "E", "W", "I", "UP", "B", "SIM", "RUF"] +ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] @@ -33,6 +22,9 @@ unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +[lint.per-file-ignores] +# marimo notebook: a bare trailing expression is how a cell renders its widget +"examples/sdk_demo.py" = ["B018"] [format] # Like Black, use double quotes for strings. diff --git a/src/codesphere/__init__.py b/src/codesphere/__init__.py index 315acac..8d3532b 100644 --- a/src/codesphere/__init__.py +++ b/src/codesphere/__init__.py @@ -56,34 +56,34 @@ logging.getLogger("codesphere").addHandler(logging.NullHandler()) __all__ = [ - "CodesphereSDK", - # Exceptions - "CodesphereError", + "APIError", "AuthenticationError", "AuthorizationError", - "NotFoundError", - "ValidationError", + "Characteristic", + # Exceptions + "CodesphereError", + "CodesphereSDK", "ConflictError", - "RateLimitError", - "APIError", + "CustomDomainConfig", + "Datacenter", + "Domain", + "DomainBase", + "DomainRouting", + "DomainVerificationStatus", + "EnvVar", + "Image", "NetworkError", - "TimeoutError", + "NotFoundError", + "RateLimitError", # Resources "Team", - "TeamCreate", "TeamBase", + "TeamCreate", + "TimeoutError", + "ValidationError", "Workspace", "WorkspaceCreate", - "WorkspaceUpdate", "WorkspaceStatus", - "EnvVar", - "Datacenter", - "Characteristic", + "WorkspaceUpdate", "WsPlan", - "Image", - "Domain", - "CustomDomainConfig", - "DomainVerificationStatus", - "DomainBase", - "DomainRouting", ] diff --git a/src/codesphere/core/__init__.py b/src/codesphere/core/__init__.py index f92ed07..542d066 100644 --- a/src/codesphere/core/__init__.py +++ b/src/codesphere/core/__init__.py @@ -3,11 +3,11 @@ from .operations import APIOperation, AsyncCallable, StreamOperation __all__ = [ - "CamelModel", - "ResourceBase", "APIOperation", - "_APIOperationExecutor", "APIRequestHandler", "AsyncCallable", + "CamelModel", + "ResourceBase", "StreamOperation", + "_APIOperationExecutor", ] diff --git a/src/codesphere/core/base.py b/src/codesphere/core/base.py index 83536c3..21c8566 100644 --- a/src/codesphere/core/base.py +++ b/src/codesphere/core/base.py @@ -1,4 +1,4 @@ -from typing import Any, Generic, List, Literal, TypeVar +from typing import Any, Generic, Literal, TypeVar import yaml from pydantic import BaseModel, ConfigDict, RootModel @@ -75,8 +75,8 @@ def to_yaml(self, *, by_alias: bool = True, exclude_none: bool = False) -> str: ) -class ResourceList(RootModel[List[ModelT]], Generic[ModelT]): - root: List[ModelT] +class ResourceList(RootModel[list[ModelT]], Generic[ModelT]): + root: list[ModelT] def __iter__(self): return iter(self.root) diff --git a/src/codesphere/core/handler.py b/src/codesphere/core/handler.py index 1a614c4..5315edf 100644 --- a/src/codesphere/core/handler.py +++ b/src/codesphere/core/handler.py @@ -1,6 +1,6 @@ -from functools import partial import logging -from typing import Any, List, Optional, Type, get_args, get_origin +from functools import partial +from typing import Any, get_args, get_origin import httpx from pydantic import BaseModel, PrivateAttr, RootModel, ValidationError @@ -13,7 +13,7 @@ class _APIOperationExecutor: - _http_client: Optional[APIHttpClient] = PrivateAttr(default=None) + _http_client: APIHttpClient | None = PrivateAttr(default=None) def __getattribute__(self, name: str) -> Any: attr = super().__getattribute__(name) @@ -106,7 +106,7 @@ def _inject_client_into_model(self, model_instance: BaseModel) -> BaseModel: async def _parse_and_validate_response( self, response: httpx.Response, - response_model: Type[BaseModel] | Type[List[BaseModel]] | None, + response_model: type[BaseModel] | type[list[BaseModel]] | None, endpoint_for_logging: str, ) -> Any: if response_model in (None, type(None)): @@ -121,7 +121,7 @@ async def _parse_and_validate_response( try: origin = get_origin(response_model) - if origin is list or origin is List: + if origin is list or origin is list: item_model = get_args(response_model)[0] instances = [item_model.model_validate(item) for item in json_response] for instance in instances: diff --git a/src/codesphere/core/operations.py b/src/codesphere/core/operations.py index a6192f7..4b072a2 100644 --- a/src/codesphere/core/operations.py +++ b/src/codesphere/core/operations.py @@ -1,13 +1,13 @@ -from typing import Awaitable, Callable, Generic, Optional, Type, TypeAlias, TypeVar +from collections.abc import Awaitable, Callable +from typing import Generic, TypeVar from pydantic import BaseModel, ConfigDict -_T = TypeVar("_T") ResponseT = TypeVar("ResponseT") InputT = TypeVar("InputT") EntryT = TypeVar("EntryT") -AsyncCallable: TypeAlias = Callable[[], Awaitable[_T]] +type AsyncCallable[_T] = Callable[[], Awaitable[_T]] class APIOperation(BaseModel, Generic[ResponseT, InputT]): @@ -15,11 +15,11 @@ class APIOperation(BaseModel, Generic[ResponseT, InputT]): method: str endpoint_template: str - response_model: Type[ResponseT] - input_model: Optional[Type[InputT]] = None + response_model: type[ResponseT] + input_model: type[InputT] | None = None class StreamOperation(BaseModel, Generic[EntryT]): model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) endpoint_template: str - entry_model: Type[EntryT] + entry_model: type[EntryT] diff --git a/src/codesphere/exceptions.py b/src/codesphere/exceptions.py index c7eee9b..1ac7cd8 100644 --- a/src/codesphere/exceptions.py +++ b/src/codesphere/exceptions.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any import httpx @@ -21,11 +21,11 @@ class AuthenticationError(CodesphereError): HTTP Status: 401 """ - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): if message is None: message = ( - "Authentication token not provided or invalid. Please pass it as an argument " - "or set the 'CS_TOKEN' environment variable." + "Authentication token not provided or invalid. Please pass it " + "as an argument or set the 'CS_TOKEN' environment variable." ) super().__init__(message) @@ -36,7 +36,7 @@ class AuthorizationError(CodesphereError): HTTP Status: 403 """ - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): if message is None: message = "You don't have permission to perform this action." super().__init__(message) @@ -48,7 +48,7 @@ class NotFoundError(CodesphereError): HTTP Status: 404 """ - def __init__(self, message: Optional[str] = None, resource: Optional[str] = None): + def __init__(self, message: str | None = None, resource: str | None = None): self.resource = resource if message is None: if resource: @@ -66,8 +66,8 @@ class ValidationError(CodesphereError): def __init__( self, - message: Optional[str] = None, - errors: Optional[list[dict[str, Any]]] = None, + message: str | None = None, + errors: list[dict[str, Any]] | None = None, ): self.errors = errors or [] if message is None: @@ -81,7 +81,7 @@ class ConflictError(CodesphereError): HTTP Status: 409 """ - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): if message is None: message = "The request conflicts with the current state of the resource." super().__init__(message) @@ -95,8 +95,8 @@ class RateLimitError(CodesphereError): def __init__( self, - message: Optional[str] = None, - retry_after: Optional[int] = None, + message: str | None = None, + retry_after: int | None = None, ): self.retry_after = retry_after if message is None: @@ -115,11 +115,11 @@ class APIError(CodesphereError): def __init__( self, - message: Optional[str] = None, - status_code: Optional[int] = None, - response_body: Optional[Any] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, + message: str | None = None, + status_code: int | None = None, + response_body: Any | None = None, + request_url: str | None = None, + request_method: str | None = None, ): self.status_code = status_code self.response_body = response_body @@ -143,7 +143,7 @@ class NetworkError(CodesphereError): """Raised for network-related issues like connection failures or timeouts.""" def __init__( - self, message: Optional[str] = None, original_error: Optional[Exception] = None + self, message: str | None = None, original_error: Exception | None = None ): self.original_error = original_error if message is None: @@ -154,7 +154,7 @@ def __init__( class TimeoutError(NetworkError): """Raised when a request times out.""" - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): if message is None: message = "The request timed out. The server may be slow or unavailable." super().__init__(message) diff --git a/src/codesphere/http_client.py b/src/codesphere/http_client.py index e53e405..aaaa764 100644 --- a/src/codesphere/http_client.py +++ b/src/codesphere/http_client.py @@ -1,6 +1,6 @@ import logging from functools import partial -from typing import Any, Optional +from typing import Any import httpx from pydantic import BaseModel @@ -15,7 +15,7 @@ class APIHttpClient: def __init__(self, base_url: str = "https://codesphere.com/api"): self._token = settings.token.get_secret_value() self._base_url = base_url or str(settings.base_url) - self._client: Optional[httpx.AsyncClient] = None + self._client: httpx.AsyncClient | None = None self._timeout_config = httpx.Timeout( settings.client_timeout_connect, read=settings.client_timeout_read @@ -68,7 +68,8 @@ async def request( try: response = await client.request(method, endpoint, **kwargs) log.debug( - f"Response: {response.status_code} {response.reason_phrase} for {method} {endpoint}" + f"Response: {response.status_code} {response.reason_phrase} " + f"for {method} {endpoint}" ) raise_for_status(response) diff --git a/src/codesphere/resources/metadata/__init__.py b/src/codesphere/resources/metadata/__init__.py index 1f4cd88..178d70d 100644 --- a/src/codesphere/resources/metadata/__init__.py +++ b/src/codesphere/resources/metadata/__init__.py @@ -1,6 +1,6 @@ """Metadata Resource & Models""" -from .schemas import Datacenter, Characteristic, WsPlan, Image from .resources import MetadataResource +from .schemas import Characteristic, Datacenter, Image, WsPlan -__all__ = ["Datacenter", "Characteristic", "WsPlan", "Image", "MetadataResource"] +__all__ = ["Characteristic", "Datacenter", "Image", "MetadataResource", "WsPlan"] diff --git a/src/codesphere/resources/metadata/resources.py b/src/codesphere/resources/metadata/resources.py index 2fa30eb..ebca89e 100644 --- a/src/codesphere/resources/metadata/resources.py +++ b/src/codesphere/resources/metadata/resources.py @@ -1,10 +1,9 @@ -from typing import List from pydantic import Field + +from ...core import AsyncCallable, ResourceBase from ...core.base import ResourceList -from ...core import AsyncCallable -from ...core import ResourceBase from .operations import _LIST_DC_OP, _LIST_IMAGES_OP, _LIST_PLANS_OP -from .schemas import Datacenter, WsPlan, Image +from .schemas import Datacenter, Image, WsPlan class MetadataResource(ResourceBase): @@ -18,14 +17,14 @@ class MetadataResource(ResourceBase): default=_LIST_IMAGES_OP, exclude=True ) - async def list_datacenters(self) -> List[Datacenter]: + async def list_datacenters(self) -> list[Datacenter]: result = await self.list_datacenters_op() return result.root - async def list_plans(self) -> List[WsPlan]: + async def list_plans(self) -> list[WsPlan]: result = await self.list_plans_op() return result.root - async def list_images(self) -> List[Image]: + async def list_images(self) -> list[Image]: result = await self.list_images_op() return result.root diff --git a/src/codesphere/resources/metadata/schemas.py b/src/codesphere/resources/metadata/schemas.py index 4628641..4450792 100644 --- a/src/codesphere/resources/metadata/schemas.py +++ b/src/codesphere/resources/metadata/schemas.py @@ -1,4 +1,5 @@ from __future__ import annotations + import datetime from pydantic import Field diff --git a/src/codesphere/resources/team/__init__.py b/src/codesphere/resources/team/__init__.py index ffe4fee..7c13b3b 100644 --- a/src/codesphere/resources/team/__init__.py +++ b/src/codesphere/resources/team/__init__.py @@ -18,20 +18,20 @@ ) __all__ = [ - "Team", - "TeamCreate", - "TeamBase", - "TeamsResource", - "Domain", "CustomDomainConfig", - "DomainVerificationStatus", + "Domain", "DomainBase", "DomainRouting", - "TeamUsageManager", + "DomainVerificationStatus", "LandscapeServiceEvent", "LandscapeServiceSummary", "PaginatedResponse", "ServiceAction", + "Team", + "TeamBase", + "TeamCreate", + "TeamUsageManager", + "TeamsResource", "UsageEventsResponse", "UsageSummaryResponse", ] diff --git a/src/codesphere/resources/team/domain/manager.py b/src/codesphere/resources/team/domain/manager.py index 2a923b5..711b3a3 100644 --- a/src/codesphere/resources/team/domain/manager.py +++ b/src/codesphere/resources/team/domain/manager.py @@ -1,5 +1,3 @@ -from typing import List, Union - from pydantic import Field from ....core.base import ResourceList @@ -21,7 +19,7 @@ def __init__(self, http_client: APIHttpClient, team_id: int): exclude=True, ) - async def list(self) -> List[Domain]: + async def list(self) -> list[Domain]: result = await self.list_op() return result.root @@ -55,7 +53,7 @@ async def update(self, name: str, config: CustomDomainConfig) -> Domain: ) async def update_workspace_connections( - self, name: str, connections: Union[DomainRouting, RoutingMap] + self, name: str, connections: DomainRouting | RoutingMap ) -> Domain: payload = ( connections.root if isinstance(connections, DomainRouting) else connections diff --git a/src/codesphere/resources/team/domain/resources.py b/src/codesphere/resources/team/domain/resources.py index f212293..da544d5 100644 --- a/src/codesphere/resources/team/domain/resources.py +++ b/src/codesphere/resources/team/domain/resources.py @@ -1,8 +1,12 @@ from __future__ import annotations + import logging -from typing import Union + from pydantic import Field +from ....core.handler import _APIOperationExecutor +from ....core.operations import AsyncCallable +from ....utils import update_model_fields from .operations import ( _DELETE_OP, _UPDATE_OP, @@ -17,11 +21,6 @@ RoutingMap, ) -from ....core.handler import _APIOperationExecutor -from ....core.operations import AsyncCallable -from ....utils import update_model_fields - - log = logging.getLogger(__name__) @@ -40,7 +39,7 @@ async def update(self, data: CustomDomainConfig) -> Domain: return response async def update_workspace_connections( - self, connections: Union[DomainRouting, RoutingMap] + self, connections: DomainRouting | RoutingMap ) -> Domain: payload = ( connections.root if isinstance(connections, DomainRouting) else connections diff --git a/src/codesphere/resources/team/domain/schemas.py b/src/codesphere/resources/team/domain/schemas.py index d6c9ab4..c48136e 100644 --- a/src/codesphere/resources/team/domain/schemas.py +++ b/src/codesphere/resources/team/domain/schemas.py @@ -1,17 +1,15 @@ from __future__ import annotations -from typing import Dict, List, Optional, TypeAlias - from pydantic import Field, RootModel from ....core.base import CamelModel -RoutingMap: TypeAlias = Dict[str, List[int]] +type RoutingMap = dict[str, list[int]] class CertificateRequestStatus(CamelModel): issued: bool - reason: Optional[str] = None + reason: str | None = None class DNSEntries(CamelModel): @@ -22,20 +20,20 @@ class DNSEntries(CamelModel): class DomainVerificationStatus(CamelModel): verified: bool - reason: Optional[str] = None + reason: str | None = None class CustomDomainConfig(CamelModel): - restricted: Optional[bool] = None - max_body_size_mb: Optional[int] = None - max_connection_timeout_s: Optional[int] = None - use_regex: Optional[bool] = None + restricted: bool | None = None + max_body_size_mb: int | None = None + max_connection_timeout_s: int | None = None + use_regex: bool | None = None class DomainRouting(RootModel): root: RoutingMap = Field(default_factory=dict) - def add(self, path: str, workspace_ids: List[int]) -> DomainRouting: + def add(self, path: str, workspace_ids: list[int]) -> DomainRouting: self.root[path] = workspace_ids return self @@ -48,5 +46,5 @@ class DomainBase(CamelModel): certificate_request_status: CertificateRequestStatus dns_entries: DNSEntries domain_verification_status: DomainVerificationStatus - custom_config_revision: Optional[int] = None - custom_config: Optional[CustomDomainConfig] = None + custom_config_revision: int | None = None + custom_config: CustomDomainConfig | None = None diff --git a/src/codesphere/resources/team/operations.py b/src/codesphere/resources/team/operations.py index ca56871..83e6455 100644 --- a/src/codesphere/resources/team/operations.py +++ b/src/codesphere/resources/team/operations.py @@ -1,6 +1,6 @@ -from .schemas import Team, TeamCreate from ...core.base import ResourceList from ...core.operations import APIOperation +from .schemas import Team, TeamCreate _LIST_TEAMS_OP = APIOperation( method="GET", diff --git a/src/codesphere/resources/team/resources.py b/src/codesphere/resources/team/resources.py index 6ee9411..2bc8097 100644 --- a/src/codesphere/resources/team/resources.py +++ b/src/codesphere/resources/team/resources.py @@ -1,15 +1,12 @@ -from typing import List - from pydantic import Field +from ...core import AsyncCallable, ResourceBase +from ...core.base import ResourceList from .operations import ( _CREATE_TEAM_OP, _GET_TEAM_OP, _LIST_TEAMS_OP, ) - -from ...core.base import ResourceList -from ...core import AsyncCallable, ResourceBase from .schemas import Team, TeamCreate @@ -18,7 +15,7 @@ class TeamsResource(ResourceBase): default=_LIST_TEAMS_OP, exclude=True ) - async def list(self) -> List[Team]: + async def list(self) -> list[Team]: result = await self.list_team_op() return result.root diff --git a/src/codesphere/resources/team/schemas.py b/src/codesphere/resources/team/schemas.py index a40713e..977b55b 100644 --- a/src/codesphere/resources/team/schemas.py +++ b/src/codesphere/resources/team/schemas.py @@ -1,7 +1,6 @@ from __future__ import annotations from functools import cached_property -from typing import Optional from pydantic import Field @@ -19,12 +18,12 @@ class TeamCreate(CamelModel): class TeamBase(CamelModel): id: int name: str - description: Optional[str] = None - avatar_id: Optional[str] = None - avatar_url: Optional[str] = None + description: str | None = None + avatar_id: str | None = None + avatar_url: str | None = None is_first: bool default_data_center_id: int - role: Optional[int] = None + role: int | None = None class Team(TeamBase, _APIOperationExecutor): diff --git a/src/codesphere/resources/team/usage/__init__.py b/src/codesphere/resources/team/usage/__init__.py index a264ba2..3e56706 100644 --- a/src/codesphere/resources/team/usage/__init__.py +++ b/src/codesphere/resources/team/usage/__init__.py @@ -11,11 +11,11 @@ ) __all__ = [ - "TeamUsageManager", "LandscapeServiceEvent", "LandscapeServiceSummary", "PaginatedResponse", "ServiceAction", + "TeamUsageManager", "UsageEventsResponse", "UsageSummaryResponse", ] diff --git a/src/codesphere/resources/team/usage/manager.py b/src/codesphere/resources/team/usage/manager.py index 2be2149..e01d7e9 100644 --- a/src/codesphere/resources/team/usage/manager.py +++ b/src/codesphere/resources/team/usage/manager.py @@ -1,8 +1,8 @@ from __future__ import annotations +from collections.abc import AsyncIterator from datetime import datetime from functools import partial -from typing import AsyncIterator, Union from pydantic import Field @@ -57,8 +57,8 @@ def __init__(self, http_client: APIHttpClient, team_id: int): async def get_landscape_summary( self, - begin_date: Union[datetime, str], - end_date: Union[datetime, str], + begin_date: datetime | str, + end_date: datetime | str, limit: int = 25, offset: int = 0, ) -> UsageSummaryResponse: @@ -83,8 +83,8 @@ async def get_landscape_summary( async def iter_all_landscape_summary( self, - begin_date: Union[datetime, str], - end_date: Union[datetime, str], + begin_date: datetime | str, + end_date: datetime | str, page_size: int = 100, ) -> AsyncIterator[LandscapeServiceSummary]: offset = 0 @@ -113,8 +113,8 @@ async def iter_all_landscape_summary( async def get_landscape_events( self, resource_id: str, - begin_date: Union[datetime, str], - end_date: Union[datetime, str], + begin_date: datetime | str, + end_date: datetime | str, limit: int = 25, offset: int = 0, ) -> UsageEventsResponse: @@ -143,8 +143,8 @@ async def get_landscape_events( async def iter_all_landscape_events( self, resource_id: str, - begin_date: Union[datetime, str], - end_date: Union[datetime, str], + begin_date: datetime | str, + end_date: datetime | str, page_size: int = 100, ) -> AsyncIterator[LandscapeServiceEvent]: offset = 0 diff --git a/src/codesphere/resources/team/usage/schemas.py b/src/codesphere/resources/team/usage/schemas.py index 74f6e3c..0bbbf4d 100644 --- a/src/codesphere/resources/team/usage/schemas.py +++ b/src/codesphere/resources/team/usage/schemas.py @@ -2,7 +2,7 @@ from datetime import datetime from enum import Enum -from typing import Generic, List, Optional, TypeVar +from typing import Generic, TypeVar from pydantic import Field @@ -43,8 +43,8 @@ class LandscapeServiceEvent(CamelModel): class PaginatedResponse(CamelModel, Generic[ItemT]): total_items: int - limit: Optional[int] = Field(default=25) - offset: Optional[int] = Field(default=0) + limit: int | None = Field(default=25) + offset: int | None = Field(default=0) begin_date: datetime end_date: datetime @@ -75,12 +75,12 @@ def total_pages(self) -> int: class UsageSummaryResponse( PaginatedResponse[LandscapeServiceSummary], _APIOperationExecutor ): - summary: List[LandscapeServiceSummary] = Field(default_factory=list) - _refresh_op: Optional[AsyncCallable[UsageSummaryResponse]] = None - _team_id: Optional[int] = None + summary: list[LandscapeServiceSummary] = Field(default_factory=list) + _refresh_op: AsyncCallable[UsageSummaryResponse] | None = None + _team_id: int | None = None @property - def items(self) -> List[LandscapeServiceSummary]: + def items(self) -> list[LandscapeServiceSummary]: return self.summary async def refresh(self) -> UsageSummaryResponse: @@ -105,14 +105,14 @@ async def refresh(self) -> UsageSummaryResponse: class UsageEventsResponse( PaginatedResponse[LandscapeServiceEvent], _APIOperationExecutor ): - events: List[LandscapeServiceEvent] = Field(default_factory=list) + events: list[LandscapeServiceEvent] = Field(default_factory=list) - _refresh_op: Optional[AsyncCallable[UsageEventsResponse]] = None - _team_id: Optional[int] = None - _resource_id: Optional[str] = None + _refresh_op: AsyncCallable[UsageEventsResponse] | None = None + _team_id: int | None = None + _resource_id: str | None = None @property - def items(self) -> List[LandscapeServiceEvent]: + def items(self) -> list[LandscapeServiceEvent]: return self.events async def refresh(self) -> UsageEventsResponse: diff --git a/src/codesphere/resources/workspace/__init__.py b/src/codesphere/resources/workspace/__init__.py index 4045de8..f8bebba 100644 --- a/src/codesphere/resources/workspace/__init__.py +++ b/src/codesphere/resources/workspace/__init__.py @@ -11,18 +11,18 @@ ) __all__ = [ - "Workspace", - "WorkspaceCreate", - "WorkspaceUpdate", - "WorkspaceStatus", - "WorkspacesResource", "CommandInput", "CommandOutput", - "WorkspaceGitManager", "GitHead", - "LogStream", - "WorkspaceLogManager", "LogEntry", "LogProblem", "LogStage", + "LogStream", + "Workspace", + "WorkspaceCreate", + "WorkspaceGitManager", + "WorkspaceLogManager", + "WorkspaceStatus", + "WorkspaceUpdate", + "WorkspacesResource", ] diff --git a/src/codesphere/resources/workspace/envVars/__init__.py b/src/codesphere/resources/workspace/envVars/__init__.py index f953883..dbd4fcb 100644 --- a/src/codesphere/resources/workspace/envVars/__init__.py +++ b/src/codesphere/resources/workspace/envVars/__init__.py @@ -1,4 +1,4 @@ -from .schemas import EnvVar from .models import WorkspaceEnvVarManager +from .schemas import EnvVar __all__ = ["EnvVar", "WorkspaceEnvVarManager"] diff --git a/src/codesphere/resources/workspace/envVars/models.py b/src/codesphere/resources/workspace/envVars/models.py index ff9bb82..36d90a1 100644 --- a/src/codesphere/resources/workspace/envVars/models.py +++ b/src/codesphere/resources/workspace/envVars/models.py @@ -1,12 +1,12 @@ from __future__ import annotations + import logging -from typing import Dict, List, Union -from .schemas import EnvVar from ....core.base import ResourceList from ....core.handler import _APIOperationExecutor from ....http_client import APIHttpClient from .operations import _BULK_DELETE_OP, _BULK_SET_OP, _GET_OP +from .schemas import EnvVar log = logging.getLogger(__name__) @@ -17,19 +17,17 @@ def __init__(self, http_client: APIHttpClient, workspace_id: int): self._workspace_id = workspace_id self.id = workspace_id - async def get(self) -> List[EnvVar]: + async def get(self) -> list[EnvVar]: return await self._execute_operation(_GET_OP) - async def set( - self, env_vars: Union[ResourceList[EnvVar], List[Dict[str, str]]] - ) -> None: + async def set(self, env_vars: ResourceList[EnvVar] | list[dict[str, str]]) -> None: payload = ResourceList[EnvVar].model_validate(env_vars) await self._execute_operation(_BULK_SET_OP, data=payload.model_dump()) - async def delete(self, items: Union[List[str], ResourceList[EnvVar]]) -> None: + async def delete(self, items: list[str] | ResourceList[EnvVar]) -> None: if not items: return - payload: List[str] = [] + payload: list[str] = [] for item in items: if isinstance(item, str): diff --git a/src/codesphere/resources/workspace/envVars/operations.py b/src/codesphere/resources/workspace/envVars/operations.py index ceca85f..35eef50 100644 --- a/src/codesphere/resources/workspace/envVars/operations.py +++ b/src/codesphere/resources/workspace/envVars/operations.py @@ -1,6 +1,6 @@ -from .schemas import EnvVar from ....core.base import ResourceList from ....core.operations import APIOperation +from .schemas import EnvVar _GET_OP = APIOperation( method="GET", diff --git a/src/codesphere/resources/workspace/git/__init__.py b/src/codesphere/resources/workspace/git/__init__.py index ea90a86..3d77710 100644 --- a/src/codesphere/resources/workspace/git/__init__.py +++ b/src/codesphere/resources/workspace/git/__init__.py @@ -1,4 +1,4 @@ from .models import WorkspaceGitManager from .schema import GitHead -__all__ = ["WorkspaceGitManager", "GitHead"] +__all__ = ["GitHead", "WorkspaceGitManager"] diff --git a/src/codesphere/resources/workspace/git/models.py b/src/codesphere/resources/workspace/git/models.py index 0492a72..4c34879 100644 --- a/src/codesphere/resources/workspace/git/models.py +++ b/src/codesphere/resources/workspace/git/models.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from typing import Optional from ....core.handler import _APIOperationExecutor from ....http_client import APIHttpClient @@ -29,8 +28,8 @@ async def get_head(self) -> GitHead: async def pull( self, - remote: Optional[str] = None, - branch: Optional[str] = None, + remote: str | None = None, + branch: str | None = None, ) -> None: if remote is not None and branch is not None: await self._execute_operation( diff --git a/src/codesphere/resources/workspace/landscape/__init__.py b/src/codesphere/resources/workspace/landscape/__init__.py index 4facd41..7060d87 100644 --- a/src/codesphere/resources/workspace/landscape/__init__.py +++ b/src/codesphere/resources/workspace/landscape/__init__.py @@ -20,22 +20,22 @@ ) __all__ = [ - "WorkspaceLandscapeManager", - "Profile", - "ProfileBuilder", - "ProfileConfig", - "Step", - "StageConfig", - "ReactiveServiceConfig", - "ReactiveServiceBuilder", - "ManagedServiceConfig", "ManagedServiceBuilder", + "ManagedServiceConfig", "NetworkConfig", - "PortConfig", "PathConfig", "PipelineStage", "PipelineState", "PipelineStatus", "PipelineStatusList", + "PortConfig", + "Profile", + "ProfileBuilder", + "ProfileConfig", + "ReactiveServiceBuilder", + "ReactiveServiceConfig", + "StageConfig", + "Step", "StepStatus", + "WorkspaceLandscapeManager", ] diff --git a/src/codesphere/resources/workspace/landscape/models.py b/src/codesphere/resources/workspace/landscape/models.py index 99e9bdc..c29fadf 100644 --- a/src/codesphere/resources/workspace/landscape/models.py +++ b/src/codesphere/resources/workspace/landscape/models.py @@ -3,7 +3,7 @@ import asyncio import logging import re -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from typing import TYPE_CHECKING from ....core.base import ResourceList from ....core.handler import _APIOperationExecutor @@ -55,7 +55,7 @@ def __init__(self, http_client: APIHttpClient, workspace_id: int): self._workspace_id = workspace_id self.id = workspace_id - async def _run_command(self, command: str) -> "CommandOutput": + async def _run_command(self, command: str) -> CommandOutput: from ..operations import _EXECUTE_COMMAND_OP from ..schemas import CommandInput @@ -66,7 +66,7 @@ async def _run_command(self, command: str) -> "CommandOutput": async def list_profiles(self) -> ResourceList[Profile]: result = await self._run_command("ls -1 *.yml 2>/dev/null || true") - profiles: List[Profile] = [] + profiles: list[Profile] = [] if result.output: for line in result.output.strip().split("\n"): if match := _PROFILE_FILE_PATTERN.match(line.strip()): @@ -74,13 +74,10 @@ async def list_profiles(self) -> ResourceList[Profile]: return ResourceList[Profile](root=profiles) - async def save_profile(self, name: str, config: Union[ProfileConfig, str]) -> None: + async def save_profile(self, name: str, config: ProfileConfig | str) -> None: filename = _profile_filename(name) - if isinstance(config, ProfileConfig): - yaml_content = config.to_yaml() - else: - yaml_content = config + yaml_content = config.to_yaml() if isinstance(config, ProfileConfig) else config body = yaml_content if yaml_content.endswith("\n") else yaml_content + "\n" await self._run_command( @@ -94,7 +91,7 @@ async def get_profile(self, name: str) -> str: async def delete_profile(self, name: str) -> None: await self._run_command(f"rm -f {_profile_filename(name)}") - async def deploy(self, profile: Optional[str] = None) -> None: + async def deploy(self, profile: str | None = None) -> None: if profile is not None: _validate_profile_name(profile) await self._execute_operation(_DEPLOY_WITH_PROFILE_OP, profile=profile) @@ -104,13 +101,13 @@ async def deploy(self, profile: Optional[str] = None) -> None: async def teardown(self) -> None: await self._execute_operation(_TEARDOWN_OP) - async def scale(self, services: Dict[str, int]) -> None: + async def scale(self, services: dict[str, int]) -> None: await self._execute_operation(_SCALE_OP, data=services) async def start_stage( self, - stage: Union[PipelineStage, str], - profile: Optional[str] = None, + stage: PipelineStage | str, + profile: str | None = None, ) -> None: if isinstance(stage, PipelineStage): stage = stage.value @@ -123,15 +120,13 @@ async def start_stage( else: await self._execute_operation(_START_PIPELINE_STAGE_OP, stage=stage) - async def stop_stage(self, stage: Union[PipelineStage, str]) -> None: + async def stop_stage(self, stage: PipelineStage | str) -> None: if isinstance(stage, PipelineStage): stage = stage.value await self._execute_operation(_STOP_PIPELINE_STAGE_OP, stage=stage) - async def get_stage_status( - self, stage: Union[PipelineStage, str] - ) -> PipelineStatusList: + async def get_stage_status(self, stage: PipelineStage | str) -> PipelineStatusList: if isinstance(stage, PipelineStage): stage = stage.value @@ -139,11 +134,11 @@ async def get_stage_status( async def wait_for_stage( self, - stage: Union[PipelineStage, str], + stage: PipelineStage | str, *, timeout: float = 300.0, poll_interval: float = 5.0, - server: Optional[str] = None, + server: str | None = None, ) -> PipelineStatusList: if poll_interval <= 0: raise ValueError("poll_interval must be greater than 0") @@ -160,9 +155,7 @@ async def wait_for_stage( if s.server == server: relevant_statuses.append(s) else: - if s.steps: - relevant_statuses.append(s) - elif s.state != PipelineState.WAITING: + if s.steps or s.state != PipelineState.WAITING: relevant_statuses.append(s) if not relevant_statuses: diff --git a/src/codesphere/resources/workspace/landscape/schemas.py b/src/codesphere/resources/workspace/landscape/schemas.py index 9baa40d..42b1789 100644 --- a/src/codesphere/resources/workspace/landscape/schemas.py +++ b/src/codesphere/resources/workspace/landscape/schemas.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Literal import yaml from pydantic import BaseModel, Field, RootModel @@ -25,21 +25,21 @@ class PipelineState(str, Enum): class StepStatus(CamelModel): state: PipelineState - started_at: Optional[str] = None - finished_at: Optional[str] = None + started_at: str | None = None + finished_at: str | None = None class PipelineStatus(CamelModel): state: PipelineState - started_at: Optional[str] = None - finished_at: Optional[str] = None - steps: List[StepStatus] = Field(default_factory=list) + started_at: str | None = None + finished_at: str | None = None + steps: list[StepStatus] = Field(default_factory=list) replica: str server: str -class PipelineStatusList(RootModel[List[PipelineStatus]]): - root: List[PipelineStatus] +class PipelineStatusList(RootModel[list[PipelineStatus]]): + root: list[PipelineStatus] def __iter__(self): return iter(self.root) @@ -56,7 +56,7 @@ class Profile(BaseModel): class Step(CamelModel): - name: Optional[str] = None + name: str | None = None command: str @@ -68,43 +68,43 @@ class PortConfig(CamelModel): class PathConfig(CamelModel): port: int = Field(ge=1, le=65535) path: str - strip_path: Optional[bool] = None + strip_path: bool | None = None class NetworkConfig(CamelModel): - ports: List[PortConfig] = Field(default_factory=list) - paths: List[PathConfig] = Field(default_factory=list) + ports: list[PortConfig] = Field(default_factory=list) + paths: list[PathConfig] = Field(default_factory=list) class ReactiveServiceConfig(CamelModel): - steps: List[Step] = Field(default_factory=list) + steps: list[Step] = Field(default_factory=list) plan: int replicas: int = 1 - env: Optional[Dict[str, str]] = None - base_image: Optional[str] = None - run_as_user: Optional[int] = Field(default=None, ge=0, le=65534) - run_as_group: Optional[int] = Field(default=None, ge=0, le=65534) - mount_sub_path: Optional[str] = None - health_endpoint: Optional[str] = None - network: Optional[NetworkConfig] = None + env: dict[str, str] | None = None + base_image: str | None = None + run_as_user: int | None = Field(default=None, ge=0, le=65534) + run_as_group: int | None = Field(default=None, ge=0, le=65534) + mount_sub_path: str | None = None + health_endpoint: str | None = None + network: NetworkConfig | None = None class ManagedServiceConfig(CamelModel): provider: str plan: str - config: Optional[Dict[str, Any]] = None - secrets: Optional[Dict[str, str]] = None + config: dict[str, Any] | None = None + secrets: dict[str, str] | None = None class StageConfig(CamelModel): - steps: List[Step] = Field(default_factory=list) + steps: list[Step] = Field(default_factory=list) class ProfileConfig(CamelModel): schema_version: Literal["v0.2"] = Field(default="v0.2", alias="schemaVersion") prepare: StageConfig = Field(default_factory=StageConfig) test: StageConfig = Field(default_factory=StageConfig) - run: Dict[str, ReactiveServiceConfig | ManagedServiceConfig] = Field( + run: dict[str, ReactiveServiceConfig | ManagedServiceConfig] = Field( default_factory=dict ) @@ -116,7 +116,7 @@ def to_yaml(self, *, exclude_none: bool = True) -> str: class StepBuilder: - def __init__(self, command: str, name: Optional[str] = None): + def __init__(self, command: str, name: str | None = None): self._command = command self._name = name @@ -141,7 +141,7 @@ class PathBuilder: def __init__(self, path: str, port: int): self._path = path self._port = port - self._strip_path: Optional[bool] = None + self._strip_path: bool | None = None def strip_path(self, strip: bool = True) -> PathBuilder: self._strip_path = strip @@ -154,25 +154,23 @@ def build(self) -> PathConfig: class ReactiveServiceBuilder: def __init__(self, name: str): self._name = name - self._steps: List[Step] = [] - self._env: Dict[str, str] = {} - self._plan: Optional[int] = None + self._steps: list[Step] = [] + self._env: dict[str, str] = {} + self._plan: int | None = None self._replicas: int = 1 - self._base_image: Optional[str] = None - self._run_as_user: Optional[int] = None - self._run_as_group: Optional[int] = None - self._mount_sub_path: Optional[str] = None - self._health_endpoint: Optional[str] = None - self._ports: List[PortConfig] = [] - self._paths: List[PathConfig] = [] + self._base_image: str | None = None + self._run_as_user: int | None = None + self._run_as_group: int | None = None + self._mount_sub_path: str | None = None + self._health_endpoint: str | None = None + self._ports: list[PortConfig] = [] + self._paths: list[PathConfig] = [] @property def name(self) -> str: return self._name - def add_step( - self, command: str, name: Optional[str] = None - ) -> ReactiveServiceBuilder: + def add_step(self, command: str, name: str | None = None) -> ReactiveServiceBuilder: self._steps.append(Step(command=command, name=name)) return self @@ -180,7 +178,7 @@ def env(self, key: str, value: str) -> ReactiveServiceBuilder: self._env[key] = value return self - def envs(self, env_vars: Dict[str, str]) -> ReactiveServiceBuilder: + def envs(self, env_vars: dict[str, str]) -> ReactiveServiceBuilder: self._env.update(env_vars) return self @@ -197,7 +195,7 @@ def base_image(self, image: str) -> ReactiveServiceBuilder: return self def run_as( - self, user: Optional[int] = None, group: Optional[int] = None + self, user: int | None = None, group: int | None = None ) -> ReactiveServiceBuilder: self._run_as_user = user self._run_as_group = group @@ -216,7 +214,7 @@ def add_port(self, port: int, *, public: bool = False) -> ReactiveServiceBuilder return self def add_path( - self, path: str, port: int, *, strip_path: Optional[bool] = None + self, path: str, port: int, *, strip_path: bool | None = None ) -> ReactiveServiceBuilder: self._paths.append(PathConfig(port=port, path=path, strip_path=strip_path)) return self @@ -252,8 +250,8 @@ def __init__(self, name: str, provider: str, plan: str): self._name = name self._provider = provider self._plan = plan - self._config: Dict[str, Any] = {} - self._secrets: Dict[str, str] = {} + self._config: dict[str, Any] = {} + self._secrets: dict[str, str] = {} @property def name(self) -> str: @@ -263,7 +261,7 @@ def config(self, key: str, value: Any) -> ManagedServiceBuilder: self._config[key] = value return self - def configs(self, config: Dict[str, Any]) -> ManagedServiceBuilder: + def configs(self, config: dict[str, Any]) -> ManagedServiceBuilder: self._config.update(config) return self @@ -271,7 +269,7 @@ def secret(self, key: str, value: str) -> ManagedServiceBuilder: self._secrets[key] = value return self - def secrets(self, secrets: Dict[str, str]) -> ManagedServiceBuilder: + def secrets(self, secrets: dict[str, str]) -> ManagedServiceBuilder: self._secrets.update(secrets) return self @@ -315,10 +313,10 @@ class ProfileBuilder: """ def __init__(self) -> None: - self._prepare_steps: List[Step] = [] - self._test_steps: List[Step] = [] - self._services: Dict[str, ReactiveServiceConfig | ManagedServiceConfig] = {} - self._current_service: Optional[Any] = None + self._prepare_steps: list[Step] = [] + self._test_steps: list[Step] = [] + self._services: dict[str, ReactiveServiceConfig | ManagedServiceConfig] = {} + self._current_service: Any | None = None def prepare(self) -> PrepareStageBuilder: return PrepareStageBuilder(self) @@ -371,7 +369,7 @@ class PrepareStageBuilder: def __init__(self, parent: ProfileBuilder): self._parent = parent - def add_step(self, command: str, name: Optional[str] = None) -> PrepareStageBuilder: + def add_step(self, command: str, name: str | None = None) -> PrepareStageBuilder: self._parent._prepare_steps.append(Step(command=command, name=name)) return self @@ -383,7 +381,7 @@ class TestStageBuilder: def __init__(self, parent: ProfileBuilder): self._parent = parent - def add_step(self, command: str, name: Optional[str] = None) -> TestStageBuilder: + def add_step(self, command: str, name: str | None = None) -> TestStageBuilder: self._parent._test_steps.append(Step(command=command, name=name)) return self @@ -397,7 +395,7 @@ def __init__(self, parent: ProfileBuilder, name: str): self._builder = ReactiveServiceBuilder(name) def add_step( - self, command: str, name: Optional[str] = None + self, command: str, name: str | None = None ) -> ReactiveServiceBuilderContext: self._builder.add_step(command, name) return self @@ -406,7 +404,7 @@ def env(self, key: str, value: str) -> ReactiveServiceBuilderContext: self._builder.env(key, value) return self - def envs(self, env_vars: Dict[str, str]) -> ReactiveServiceBuilderContext: + def envs(self, env_vars: dict[str, str]) -> ReactiveServiceBuilderContext: self._builder.envs(env_vars) return self @@ -423,7 +421,7 @@ def base_image(self, image: str) -> ReactiveServiceBuilderContext: return self def run_as( - self, user: Optional[int] = None, group: Optional[int] = None + self, user: int | None = None, group: int | None = None ) -> ReactiveServiceBuilderContext: self._builder.run_as(user, group) return self @@ -443,7 +441,7 @@ def add_port( return self def add_path( - self, path: str, port: int, *, strip_path: Optional[bool] = None + self, path: str, port: int, *, strip_path: bool | None = None ) -> ReactiveServiceBuilderContext: self._builder.add_path(path, port, strip_path=strip_path) return self @@ -463,7 +461,7 @@ def config(self, key: str, value: Any) -> ManagedServiceBuilderContext: self._builder.config(key, value) return self - def configs(self, config: Dict[str, Any]) -> ManagedServiceBuilderContext: + def configs(self, config: dict[str, Any]) -> ManagedServiceBuilderContext: self._builder.configs(config) return self @@ -471,7 +469,7 @@ def secret(self, key: str, value: str) -> ManagedServiceBuilderContext: self._builder.secret(key, value) return self - def secrets(self, secrets: Dict[str, str]) -> ManagedServiceBuilderContext: + def secrets(self, secrets: dict[str, str]) -> ManagedServiceBuilderContext: self._builder.secrets(secrets) return self diff --git a/src/codesphere/resources/workspace/logs/__init__.py b/src/codesphere/resources/workspace/logs/__init__.py index 27ce1d5..e4836b9 100644 --- a/src/codesphere/resources/workspace/logs/__init__.py +++ b/src/codesphere/resources/workspace/logs/__init__.py @@ -2,9 +2,9 @@ from .schemas import LogEntry, LogProblem, LogStage __all__ = [ - "LogStream", - "WorkspaceLogManager", "LogEntry", "LogProblem", "LogStage", + "LogStream", + "WorkspaceLogManager", ] diff --git a/src/codesphere/resources/workspace/logs/models.py b/src/codesphere/resources/workspace/logs/models.py index 8f703ac..1837570 100644 --- a/src/codesphere/resources/workspace/logs/models.py +++ b/src/codesphere/resources/workspace/logs/models.py @@ -3,7 +3,7 @@ import asyncio import json import logging -from typing import AsyncIterator, Optional, Type, Union +from collections.abc import AsyncIterator import httpx @@ -27,14 +27,14 @@ def __init__( self, client: httpx.AsyncClient, endpoint: str, - entry_model: Type[LogEntry], - timeout: Optional[float] = None, + entry_model: type[LogEntry], + timeout: float | None = None, ): self._client = client self._endpoint = endpoint self._entry_model = entry_model self._timeout = timeout - self._response: Optional[httpx.Response] = None + self._response: httpx.Response | None = None self._stream_context = None async def __aenter__(self) -> LogStream: @@ -73,7 +73,7 @@ async def _iterate(self) -> AsyncIterator[LogEntry]: yield entry async def _parse_sse_stream(self) -> AsyncIterator[LogEntry]: - event_type: Optional[str] = None + event_type: str | None = None data_buffer: list[str] = [] async for line in self._response.aiter_lines(): @@ -103,9 +103,8 @@ async def _parse_sse_stream(self) -> AsyncIterator[LogEntry]: event_type = line[6:].strip() elif line.startswith("data:"): data_buffer.append(line[5:].strip()) - elif not line.startswith(":"): - if event_type: - data_buffer.append(line) + elif not line.startswith(":") and event_type: + data_buffer.append(line) def _parse_data(self, data_str: str) -> list[LogEntry]: entries = [] @@ -138,8 +137,8 @@ def _handle_problem(self, data_str: str) -> None: status_code=problem.status, response_body=problem_data, ) - except json.JSONDecodeError: - raise APIError(message=f"Invalid problem event: {data_str}") + except json.JSONDecodeError as e: + raise APIError(message=f"Invalid problem event: {data_str}") from e class WorkspaceLogManager: @@ -156,7 +155,7 @@ def _build_endpoint(self, operation: StreamOperation, **kwargs) -> str: def _open_stream( self, operation: StreamOperation, - timeout: Optional[float] = None, + timeout: float | None = None, **kwargs, ) -> LogStream: endpoint = self._build_endpoint(operation, **kwargs) @@ -169,9 +168,9 @@ def _open_stream( def open_stream( self, - stage: Union[LogStage, str], + stage: LogStage | str, step: int, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> LogStream: """Open a log stream as an async context manager.""" if isinstance(stage, LogStage): @@ -184,7 +183,7 @@ def open_server_stream( self, step: int, server: str, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> LogStream: """Open a server log stream as an async context manager.""" return self._open_stream( @@ -195,7 +194,7 @@ def open_replica_stream( self, step: int, replica: str, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> LogStream: """Open a replica log stream as an async context manager.""" return self._open_stream( @@ -204,9 +203,9 @@ def open_replica_stream( async def stream( self, - stage: Union[LogStage, str], + stage: LogStage | str, step: int, - timeout: Optional[float] = 30.0, + timeout: float | None = 30.0, ) -> AsyncIterator[LogEntry]: """Stream logs for a given stage and step.""" async with self.open_stream(stage, step, timeout) as stream: @@ -217,7 +216,7 @@ async def stream_server( self, step: int, server: str, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> AsyncIterator[LogEntry]: """Stream run logs for a specific server.""" async with self.open_server_stream(step, server, timeout) as stream: @@ -228,7 +227,7 @@ async def stream_replica( self, step: int, replica: str, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> AsyncIterator[LogEntry]: """Stream run logs for a specific replica.""" async with self.open_replica_stream(step, replica, timeout) as stream: @@ -237,10 +236,10 @@ async def stream_replica( async def collect( self, - stage: Union[LogStage, str], + stage: LogStage | str, step: int, - max_entries: Optional[int] = None, - timeout: Optional[float] = 30.0, + max_entries: int | None = None, + timeout: float | None = 30.0, ) -> list[LogEntry]: """Collect all logs for a stage and step into a list.""" entries: list[LogEntry] = [] @@ -250,7 +249,7 @@ async def collect( entries.append(entry) if max_entries and len(entries) >= max_entries: break - except asyncio.TimeoutError: + except TimeoutError: pass return entries @@ -258,8 +257,8 @@ async def collect_server( self, step: int, server: str, - max_entries: Optional[int] = None, - timeout: Optional[float] = 30.0, + max_entries: int | None = None, + timeout: float | None = 30.0, ) -> list[LogEntry]: """Collect all logs for a server into a list.""" entries: list[LogEntry] = [] @@ -269,7 +268,7 @@ async def collect_server( entries.append(entry) if max_entries and len(entries) >= max_entries: break - except asyncio.TimeoutError: + except TimeoutError: pass return entries @@ -277,8 +276,8 @@ async def collect_replica( self, step: int, replica: str, - max_entries: Optional[int] = None, - timeout: Optional[float] = 30.0, + max_entries: int | None = None, + timeout: float | None = 30.0, ) -> list[LogEntry]: """Collect all logs for a replica into a list.""" entries: list[LogEntry] = [] @@ -288,6 +287,6 @@ async def collect_replica( entries.append(entry) if max_entries and len(entries) >= max_entries: break - except asyncio.TimeoutError: + except TimeoutError: pass return entries diff --git a/src/codesphere/resources/workspace/logs/schemas.py b/src/codesphere/resources/workspace/logs/schemas.py index f54eb61..25762c9 100644 --- a/src/codesphere/resources/workspace/logs/schemas.py +++ b/src/codesphere/resources/workspace/logs/schemas.py @@ -1,7 +1,8 @@ from __future__ import annotations from enum import Enum -from typing import Optional + +from pydantic import ConfigDict from ....core.base import CamelModel @@ -13,11 +14,11 @@ class LogStage(str, Enum): class LogEntry(CamelModel): - model_config = {"extra": "allow"} + model_config = ConfigDict(extra="allow") - timestamp: Optional[str] = None - kind: Optional[str] = None # "I" for info, "E" for error - data: Optional[str] = None # The actual log content + timestamp: str | None = None + kind: str | None = None # "I" for info, "E" for error + data: str | None = None # The actual log content def get_text(self) -> str: return self.data or "" @@ -26,4 +27,4 @@ def get_text(self) -> str: class LogProblem(CamelModel): status: int reason: str - detail: Optional[str] = None + detail: str | None = None diff --git a/src/codesphere/resources/workspace/operations.py b/src/codesphere/resources/workspace/operations.py index fb06190..181b268 100644 --- a/src/codesphere/resources/workspace/operations.py +++ b/src/codesphere/resources/workspace/operations.py @@ -1,5 +1,7 @@ from __future__ import annotations +from ...core.base import ResourceList +from ...core.operations import APIOperation from .schemas import ( CommandInput, CommandOutput, @@ -7,8 +9,6 @@ WorkspaceCreate, WorkspaceStatus, ) -from ...core.base import ResourceList -from ...core.operations import APIOperation _LIST_BY_TEAM_OP = APIOperation( method="GET", diff --git a/src/codesphere/resources/workspace/resources.py b/src/codesphere/resources/workspace/resources.py index 0a0178c..6a4658a 100644 --- a/src/codesphere/resources/workspace/resources.py +++ b/src/codesphere/resources/workspace/resources.py @@ -1,5 +1,3 @@ -from typing import List - from pydantic import Field from ...core import ResourceBase @@ -19,7 +17,7 @@ class WorkspacesResource(ResourceBase): default=_LIST_BY_TEAM_OP, exclude=True ) - async def list(self, team_id: int) -> List[Workspace]: + async def list(self, team_id: int) -> list[Workspace]: if team_id <= 0: raise ValidationError("team_id must be a positive integer") result = await self.list_by_team_op(team_id=team_id) diff --git a/src/codesphere/resources/workspace/schemas.py b/src/codesphere/resources/workspace/schemas.py index ded34ce..7c61642 100644 --- a/src/codesphere/resources/workspace/schemas.py +++ b/src/codesphere/resources/workspace/schemas.py @@ -3,7 +3,6 @@ import asyncio import logging from functools import cached_property -from typing import Dict, List, Optional from ...core import _APIOperationExecutor from ...core.base import CamelModel @@ -20,17 +19,17 @@ class WorkspaceCreate(CamelModel): team_id: int name: str plan_id: int - base_image: Optional[str] = None + base_image: str | None = None is_private_repo: bool = True replicas: int = 1 - git_url: Optional[str] = None - initial_branch: Optional[str] = None - clone_depth: Optional[int] = None - source_workspace_id: Optional[int] = None - welcome_message: Optional[str] = None - vpn_config: Optional[str] = None - restricted: Optional[bool] = None - env: Optional[List[EnvVar]] = None + git_url: str | None = None + initial_branch: str | None = None + clone_depth: int | None = None + source_workspace_id: int | None = None + welcome_message: str | None = None + vpn_config: str | None = None + restricted: bool | None = None + env: list[EnvVar] | None = None class WorkspaceBase(CamelModel): @@ -40,29 +39,29 @@ class WorkspaceBase(CamelModel): plan_id: int is_private_repo: bool replicas: int - base_image: Optional[str] = None + base_image: str | None = None data_center_id: int user_id: int - git_url: Optional[str] = None - initial_branch: Optional[str] = None - source_workspace_id: Optional[int] = None - welcome_message: Optional[str] = None - vpn_config: Optional[str] = None + git_url: str | None = None + initial_branch: str | None = None + source_workspace_id: int | None = None + welcome_message: str | None = None + vpn_config: str | None = None restricted: bool class WorkspaceUpdate(CamelModel): - plan_id: Optional[int] = None - base_image: Optional[str] = None - name: Optional[str] = None - replicas: Optional[int] = None - vpn_config: Optional[str] = None - restricted: Optional[bool] = None + plan_id: int | None = None + base_image: str | None = None + name: str | None = None + replicas: int | None = None + vpn_config: str | None = None + restricted: bool | None = None class CommandInput(CamelModel): command: str - env: Optional[Dict[str, str]] = None + env: dict[str, str] | None = None class CommandOutput(CamelModel): @@ -122,7 +121,7 @@ async def wait_until_running( ) async def execute_command( - self, command: str, env: Optional[Dict[str, str]] = None + self, command: str, env: dict[str, str] | None = None ) -> CommandOutput: from .operations import _EXECUTE_COMMAND_OP diff --git a/src/codesphere/utils.py b/src/codesphere/utils.py index baa6024..7db3bcf 100644 --- a/src/codesphere/utils.py +++ b/src/codesphere/utils.py @@ -1,6 +1,7 @@ import logging +from typing import Any, TypeVar + from pydantic import BaseModel -from typing import Any, Dict, List, Type, TypeVar log = logging.getLogger(__name__) @@ -18,11 +19,11 @@ def update_model_fields(target: BaseModel, source: BaseModel) -> None: def dict_to_model_list( - data: Dict[Any, Any], - model_cls: Type[T], - key_field: str = None, - value_field: str = None, -) -> List[T]: + data: dict[Any, Any], + model_cls: type[T], + key_field: str | None = None, + value_field: str | None = None, +) -> list[T]: if key_field is None or value_field is None: for name, field_info in model_cls.model_fields.items(): if field_info.json_schema_extra: diff --git a/tests/conftest.py b/tests/conftest.py index 5c83c9e..75623ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import httpx @@ -11,7 +11,7 @@ class MockResponseFactory: @staticmethod def create( status_code: int = 200, - json_data: Optional[Any] = None, + json_data: Any | None = None, raise_for_status: bool = False, ) -> AsyncMock: """Create a mock httpx.Response.""" @@ -46,9 +46,9 @@ class MockHTTPClientFactory: @staticmethod def create( - response: Optional[AsyncMock] = None, + response: AsyncMock | None = None, status_code: int = 200, - json_data: Optional[Any] = None, + json_data: Any | None = None, ) -> AsyncMock: """Create a mock httpx.AsyncClient.""" mock_client = AsyncMock(spec=httpx.AsyncClient) @@ -256,7 +256,7 @@ def _create(response_data: Any): @pytest.fixture def team_model_factory(mock_http_client_for_resource, sample_team_data): - def _create(response_data: Any = None, team_data: dict = None): + def _create(response_data: Any = None, team_data: dict | None = None): from codesphere.resources.team import Team data = team_data or sample_team_data @@ -272,7 +272,7 @@ def _create(response_data: Any = None, team_data: dict = None): @pytest.fixture def workspace_model_factory(mock_http_client_for_resource, sample_workspace_data): - def _create(response_data: Any = None, workspace_data: dict = None): + def _create(response_data: Any = None, workspace_data: dict | None = None): from codesphere.resources.workspace import Workspace data = workspace_data or sample_workspace_data @@ -288,7 +288,7 @@ def _create(response_data: Any = None, workspace_data: dict = None): @pytest.fixture def domain_model_factory(mock_http_client_for_resource, sample_domain_data): - def _create(response_data: Any = None, domain_data: dict = None): + def _create(response_data: Any = None, domain_data: dict | None = None): from codesphere.resources.team.domain.resources import Domain data = domain_data or sample_domain_data diff --git a/tests/core/test_base.py b/tests/core/test_base.py index 9e66eb7..a2dc6da 100644 --- a/tests/core/test_base.py +++ b/tests/core/test_base.py @@ -1,6 +1,6 @@ import json from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import MagicMock import pytest @@ -327,7 +327,7 @@ class Item(CamelModel): item_id: int created_at: datetime - dt = datetime(2026, 2, 7, 12, 30, 45, tzinfo=timezone.utc) + dt = datetime(2026, 2, 7, 12, 30, 45, tzinfo=UTC) items = [Item(item_id=1, created_at=dt)] resource_list = ResourceList[Item](root=items) result = resource_list.to_json() @@ -342,7 +342,7 @@ class Item(CamelModel): item_id: int created_at: datetime - dt = datetime(2026, 2, 7, 12, 30, 45, tzinfo=timezone.utc) + dt = datetime(2026, 2, 7, 12, 30, 45, tzinfo=UTC) items = [Item(item_id=1, created_at=dt)] resource_list = ResourceList[Item](root=items) result = resource_list.to_list(mode="json") @@ -356,7 +356,7 @@ class Item(CamelModel): item_id: int created_at: datetime - dt = datetime(2026, 2, 7, 12, 30, 45, tzinfo=timezone.utc) + dt = datetime(2026, 2, 7, 12, 30, 45, tzinfo=UTC) items = [Item(item_id=1, created_at=dt)] resource_list = ResourceList[Item](root=items) result = resource_list.to_list() diff --git a/tests/core/test_handler.py b/tests/core/test_handler.py index 815e56f..79c8f0c 100644 --- a/tests/core/test_handler.py +++ b/tests/core/test_handler.py @@ -1,10 +1,9 @@ -import pytest -from typing import Optional, List from unittest.mock import MagicMock +import pytest from pydantic import BaseModel, Field, PrivateAttr, RootModel -from codesphere.core.handler import _APIOperationExecutor, APIRequestHandler +from codesphere.core.handler import APIRequestHandler, _APIOperationExecutor from codesphere.core.operations import APIOperation, AsyncCallable @@ -20,7 +19,7 @@ class SampleInputModel(BaseModel): class ConcreteExecutor(_APIOperationExecutor, BaseModel): id: int = 100 - _http_client: Optional[MagicMock] = PrivateAttr(default=None) + _http_client: MagicMock | None = PrivateAttr(default=None) class TestAPIOperationExecutor: @@ -33,7 +32,7 @@ def test_http_client_private_attribute_exists(self): def test_getattribute_returns_partial_for_operation(self): class ExecutorWithOp(_APIOperationExecutor, BaseModel): id: int = 123 - _http_client: Optional[MagicMock] = PrivateAttr(default=None) + _http_client: MagicMock | None = PrivateAttr(default=None) test_op: AsyncCallable[SampleResponseModel] = Field( default=APIOperation( method="GET", @@ -51,7 +50,7 @@ def test_getattribute_returns_normal_values(self): class SampleExecutor(_APIOperationExecutor, BaseModel): id: int = 456 name: str = "test" - _http_client: Optional[MagicMock] = PrivateAttr(default=None) + _http_client: MagicMock | None = PrivateAttr(default=None) executor = SampleExecutor() assert executor.id == 456 @@ -142,7 +141,7 @@ async def test_inject_client_into_model(self, mock_executor, sample_operation): class ModelWithClient(BaseModel): id: int - _http_client: Optional[MagicMock] = PrivateAttr(default=None) + _http_client: MagicMock | None = PrivateAttr(default=None) instance = ModelWithClient(id=1) handler._inject_client_into_model(instance) @@ -152,7 +151,7 @@ class ModelWithClient(BaseModel): async def test_inject_client_into_root_model_items( self, mock_executor, sample_operation ): - """RootModel containers should have _http_client injected into each item in .root""" + """RootModel items in .root should have _http_client injected.""" mock_client = MagicMock() mock_client.request = MagicMock() mock_executor._http_client = mock_client @@ -165,10 +164,10 @@ async def test_inject_client_into_root_model_items( class ItemWithClient(BaseModel): id: int - _http_client: Optional[MagicMock] = PrivateAttr(default=None) + _http_client: MagicMock | None = PrivateAttr(default=None) - class ResourceList(RootModel[List[ItemWithClient]]): - _http_client: Optional[MagicMock] = PrivateAttr(default=None) + class ResourceList(RootModel[list[ItemWithClient]]): + _http_client: MagicMock | None = PrivateAttr(default=None) item1 = ItemWithClient(id=1) item2 = ItemWithClient(id=2) diff --git a/tests/core/test_operations.py b/tests/core/test_operations.py index 2d13d07..4f53c9e 100644 --- a/tests/core/test_operations.py +++ b/tests/core/test_operations.py @@ -1,8 +1,7 @@ from dataclasses import dataclass -from typing import Optional, Type import pytest -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from codesphere.core.operations import APIOperation, StreamOperation from codesphere.resources.workspace.logs import LogEntry @@ -29,8 +28,8 @@ class APIOperationTestCase: name: str method: str endpoint_template: str - response_model: Type - input_model: Optional[Type] = None + response_model: type + input_model: type | None = None api_operation_test_cases = [ @@ -139,11 +138,8 @@ def test_api_operation_is_frozen(self): endpoint_template="/test", response_model=LogEntry, ) - try: + with pytest.raises(ValidationError): op.method = "POST" - assert False, "Should raise error" - except Exception: - pass class TestStreamOperation: @@ -160,8 +156,5 @@ def test_stream_operation_is_frozen(self): endpoint_template="/logs/{id}", entry_model=LogEntry, ) - try: + with pytest.raises(ValidationError): op.endpoint_template = "/other" - assert False, "Should raise error" - except Exception: - pass diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 2ba2ee1..f7a6ca7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,6 +1,7 @@ +import contextlib import logging import os -from typing import AsyncGenerator, List, Optional +from collections.abc import AsyncGenerator import pytest from dotenv import load_dotenv @@ -51,7 +52,7 @@ def integration_token() -> str: @pytest.fixture(scope="session") -def integration_team_id() -> Optional[int]: +def integration_team_id() -> int | None: team_id = os.environ.get("CS_TEST_TEAM_ID") return int(team_id) if team_id else None @@ -86,7 +87,7 @@ async def session_sdk_client(integration_token) -> AsyncGenerator[CodesphereSDK, @pytest.fixture(scope="session") async def test_team_id( session_sdk_client: CodesphereSDK, - integration_team_id: Optional[int], + integration_team_id: int | None, ) -> int: if integration_team_id: return integration_team_id @@ -115,8 +116,8 @@ async def test_workspaces( session_sdk_client: CodesphereSDK, test_team_id: int, test_plan_id: int, -) -> AsyncGenerator[List[Workspace], None]: - created_workspaces: List[Workspace] = [] +) -> AsyncGenerator[list[Workspace], None]: + created_workspaces: list[Workspace] = [] workspace_configs = [ {"name": f"{TEST_WORKSPACE_PREFIX}-1", "git_url": None}, @@ -140,10 +141,8 @@ async def test_workspaces( except Exception as e: log.error(f"Failed to create test workspace {config['name']}: {e}") for ws in created_workspaces: - try: + with contextlib.suppress(Exception): await ws.delete() - except Exception: - pass pytest.fail(f"Failed to create test workspaces: {e}") yield created_workspaces @@ -158,12 +157,12 @@ async def test_workspaces( @pytest.fixture(scope="session") -async def test_workspace(test_workspaces: List[Workspace]) -> Workspace: +async def test_workspace(test_workspaces: list[Workspace]) -> Workspace: return test_workspaces[0] @pytest.fixture(scope="session") -def git_workspace_id(test_workspaces: List[Workspace]) -> int: +def git_workspace_id(test_workspaces: list[Workspace]) -> int: return test_workspaces[1].id diff --git a/tests/integration/test_domains.py b/tests/integration/test_domains.py index d6ff61a..4d44023 100644 --- a/tests/integration/test_domains.py +++ b/tests/integration/test_domains.py @@ -1,10 +1,10 @@ -import pytest import time +import pytest + from codesphere import CodesphereSDK from codesphere.resources.team.domain.resources import Domain - pytestmark = [pytest.mark.integration, pytest.mark.asyncio] TEST_DOMAIN_PREFIX = "sdk-test" diff --git a/tests/integration/test_env_vars.py b/tests/integration/test_env_vars.py index 846d5b6..ed0d16b 100644 --- a/tests/integration/test_env_vars.py +++ b/tests/integration/test_env_vars.py @@ -3,7 +3,6 @@ from codesphere import CodesphereSDK from codesphere.resources.workspace import Workspace - pytestmark = [pytest.mark.integration, pytest.mark.asyncio] diff --git a/tests/integration/test_landscape.py b/tests/integration/test_landscape.py index 0e63f4c..e256278 100644 --- a/tests/integration/test_landscape.py +++ b/tests/integration/test_landscape.py @@ -45,9 +45,7 @@ async def test_list_profiles_after_creating_profile_file( workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id) profile_name = "sdk-test-profile" - create_result = await workspace.execute_command( - f"echo 'version: 1' > ci.{profile_name}.yml" - ) + await workspace.execute_command(f"echo 'version: 1' > ci.{profile_name}.yml") try: profiles = await workspace.landscape.list_profiles() diff --git a/tests/integration/test_metadata.py b/tests/integration/test_metadata.py index 9c06309..716d05c 100644 --- a/tests/integration/test_metadata.py +++ b/tests/integration/test_metadata.py @@ -1,7 +1,6 @@ import pytest -from codesphere.resources.metadata import Datacenter, WsPlan, Image - +from codesphere.resources.metadata import Datacenter, Image, WsPlan pytestmark = [pytest.mark.integration, pytest.mark.asyncio] diff --git a/tests/integration/test_teams.py b/tests/integration/test_teams.py index 193ff6d..3b766a7 100644 --- a/tests/integration/test_teams.py +++ b/tests/integration/test_teams.py @@ -3,7 +3,6 @@ from codesphere import CodesphereSDK from codesphere.resources.team import Team - pytestmark = [pytest.mark.integration, pytest.mark.asyncio] diff --git a/tests/integration/test_usage.py b/tests/integration/test_usage.py index 02a6fb4..504e385 100644 --- a/tests/integration/test_usage.py +++ b/tests/integration/test_usage.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest @@ -49,7 +49,7 @@ async def test_get_landscape_summary_returns_response( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=7) result = await team.usage.get_landscape_summary( @@ -69,7 +69,7 @@ async def test_get_landscape_summary_with_pagination( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=7) result = await team.usage.get_landscape_summary( @@ -89,7 +89,7 @@ async def test_get_landscape_summary_pagination_helpers( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=30) result = await team.usage.get_landscape_summary( @@ -113,7 +113,7 @@ async def test_get_landscape_summary_items_are_typed( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=30) result = await team.usage.get_landscape_summary( @@ -137,7 +137,7 @@ async def test_get_landscape_events_returns_response( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=30) summary = await team.usage.get_landscape_summary( @@ -166,7 +166,7 @@ async def test_get_landscape_events_items_are_typed( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=30) summary = await team.usage.get_landscape_summary( @@ -206,7 +206,7 @@ async def test_deployment_generates_usage_events( team = await sdk_client.teams.get(team_id=test_team_id) profile_name = "sdk-usage-test" - before_deploy = datetime.now(timezone.utc) + before_deploy = datetime.now(UTC) profile = ( ProfileBuilder() @@ -233,7 +233,7 @@ async def test_deployment_generates_usage_events( await asyncio.sleep(2) - after_teardown = datetime.now(timezone.utc) + after_teardown = datetime.now(UTC) summary = await team.usage.get_landscape_summary( begin_date=before_deploy, @@ -255,7 +255,7 @@ async def test_iter_all_landscape_summary( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=30) items = [] @@ -280,7 +280,7 @@ async def test_iter_all_landscape_events( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=30) summary = await team.usage.get_landscape_summary( @@ -319,7 +319,7 @@ async def test_usage_summary_refresh( ): team = await sdk_client.teams.get(team_id=test_team_id) - end_date = datetime.now(timezone.utc) + end_date = datetime.now(UTC) begin_date = end_date - timedelta(days=7) result = await team.usage.get_landscape_summary( @@ -327,8 +327,6 @@ async def test_usage_summary_refresh( end_date=end_date, ) - original_total = result.total_items - refreshed = await result.refresh() assert isinstance(refreshed, UsageSummaryResponse) diff --git a/tests/integration/test_workspaces.py b/tests/integration/test_workspaces.py index c396556..9965fb0 100644 --- a/tests/integration/test_workspaces.py +++ b/tests/integration/test_workspaces.py @@ -1,15 +1,13 @@ import pytest -from typing import List from codesphere import CodesphereSDK from codesphere.resources.workspace import ( + CommandOutput, Workspace, - WorkspaceUpdate, WorkspaceStatus, - CommandOutput, + WorkspaceUpdate, ) - pytestmark = [pytest.mark.integration, pytest.mark.asyncio] @@ -20,7 +18,7 @@ async def test_list_workspaces_by_team( self, sdk_client: CodesphereSDK, test_team_id: int, - test_workspaces: List[Workspace], + test_workspaces: list[Workspace], ): """Should retrieve a list of workspaces for a team.""" workspaces = await sdk_client.workspaces.list(team_id=test_team_id) @@ -121,7 +119,7 @@ class TestWorkspaceUpdateOperations: async def test_update_workspace_name( self, sdk_client: CodesphereSDK, - test_workspaces: List[Workspace], + test_workspaces: list[Workspace], ): """Should update an existing workspace's name.""" workspace = await sdk_client.workspaces.get(workspace_id=test_workspaces[1].id) @@ -143,7 +141,7 @@ async def test_update_workspace_name( async def test_update_workspace_replicas( self, sdk_client: CodesphereSDK, - test_workspaces: List[Workspace], + test_workspaces: list[Workspace], ): """Should update workspace replica count.""" workspace = await sdk_client.workspaces.get(workspace_id=test_workspaces[1].id) diff --git a/tests/resources/conftest.py b/tests/resources/conftest.py index 98f981e..d51a7ee 100644 --- a/tests/resources/conftest.py +++ b/tests/resources/conftest.py @@ -1,7 +1,8 @@ -import pytest -from typing import Any, Dict +from typing import Any from unittest.mock import AsyncMock, MagicMock +import pytest + class ResourceTestHelper: """ @@ -107,7 +108,7 @@ def _create(response_data: Any): def team_model_factory(mock_http_client_for_resource, sample_team_data): """Factory for creating Team model instances with mock HTTP client.""" - def _create(response_data: Any = None, team_data: Dict = None): + def _create(response_data: Any = None, team_data: dict | None = None): from codesphere.resources.team import Team data = team_data or sample_team_data @@ -123,7 +124,7 @@ def _create(response_data: Any = None, team_data: Dict = None): def workspace_model_factory(mock_http_client_for_resource, sample_workspace_data): """Factory for creating Workspace model instances with mock HTTP client.""" - def _create(response_data: Any = None, workspace_data: Dict = None): + def _create(response_data: Any = None, workspace_data: dict | None = None): from codesphere.resources.workspace import Workspace data = workspace_data or sample_workspace_data @@ -139,7 +140,7 @@ def _create(response_data: Any = None, workspace_data: Dict = None): def domain_model_factory(mock_http_client_for_resource, sample_domain_data): """Factory for creating Domain model instances with mock HTTP client.""" - def _create(response_data: Any = None, domain_data: Dict = None): + def _create(response_data: Any = None, domain_data: dict | None = None): from codesphere.resources.team.domain.resources import Domain data = domain_data or sample_domain_data diff --git a/tests/resources/metadata/test_metadata.py b/tests/resources/metadata/test_metadata.py index 385cff2..c5818ad 100644 --- a/tests/resources/metadata/test_metadata.py +++ b/tests/resources/metadata/test_metadata.py @@ -1,11 +1,11 @@ -import pytest from dataclasses import dataclass -from typing import List, Type + +import pytest from codesphere.resources.metadata import ( Datacenter, - WsPlan, Image, + WsPlan, ) @@ -15,9 +15,9 @@ class MetadataListTestCase: name: str operation: str - mock_response: List[dict] + mock_response: list[dict] expected_count: int - expected_type: Type + expected_type: type metadata_list_test_cases = [ diff --git a/tests/resources/team/domain/test_domain.py b/tests/resources/team/domain/test_domain.py index 4685e7a..f513f91 100644 --- a/tests/resources/team/domain/test_domain.py +++ b/tests/resources/team/domain/test_domain.py @@ -1,7 +1,7 @@ import pytest -from codesphere.resources.team.domain.resources import Domain from codesphere.resources.team.domain.manager import TeamDomainManager +from codesphere.resources.team.domain.resources import Domain from codesphere.resources.team.domain.schemas import ( CustomDomainConfig, DomainRouting, @@ -106,7 +106,7 @@ async def test_update_domain(self, domain_model_factory, sample_domain_data): domain, mock_client = domain_model_factory(response_data=sample_domain_data) config = CustomDomainConfig(max_body_size_mb=100) - result = await domain.update(data=config) + await domain.update(data=config) mock_client.request.assert_awaited_once() diff --git a/tests/resources/workspace/env_vars/test_env_vars.py b/tests/resources/workspace/env_vars/test_env_vars.py index f4f2f7b..5112246 100644 --- a/tests/resources/workspace/env_vars/test_env_vars.py +++ b/tests/resources/workspace/env_vars/test_env_vars.py @@ -1,6 +1,7 @@ -import pytest from dataclasses import dataclass -from typing import Any, Optional +from typing import Any + +import pytest from codesphere.resources.workspace.envVars import EnvVar, WorkspaceEnvVarManager @@ -11,8 +12,8 @@ class EnvVarOperationTestCase: name: str operation: str - input_data: Optional[Any] = None - mock_response: Optional[Any] = None + input_data: Any | None = None + mock_response: Any | None = None class TestWorkspaceEnvVarManager: @@ -30,7 +31,7 @@ async def test_get_env_vars(self, env_var_manager, sample_env_var_data): """Get should return a list of EnvVar models.""" manager, mock_client = env_var_manager - result = await manager.get() + await manager.get() mock_client.request.assert_awaited_once() diff --git a/tests/resources/workspace/test_workspace.py b/tests/resources/workspace/test_workspace.py index 2a523e6..994eaba 100644 --- a/tests/resources/workspace/test_workspace.py +++ b/tests/resources/workspace/test_workspace.py @@ -3,8 +3,8 @@ from codesphere.resources.workspace import ( Workspace, WorkspaceCreate, - WorkspaceUpdate, WorkspaceStatus, + WorkspaceUpdate, ) @@ -67,7 +67,7 @@ async def test_create_workspace( async def test_list_items_have_http_client_injected( self, workspaces_resource_factory, sample_workspace_list_data ): - """Items returned from list() should have _http_client injected and be able to call instance methods.""" + """Items from list() should get _http_client injected for instance calls.""" resource, mock_client = workspaces_resource_factory(sample_workspace_list_data) result = await resource.list(team_id=12345) diff --git a/tests/test_client.py b/tests/test_client.py index c4ab0e7..e6bfa90 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Optional, Type +from typing import Any from unittest.mock import AsyncMock, patch import httpx @@ -25,7 +25,7 @@ class RequestTestCase: use_context_manager: bool payload: Any = None mock_status_code: int = 200 - expected_exception: Optional[Type[Exception]] = None + expected_exception: type[Exception] | None = None request_test_cases = [ @@ -114,15 +114,15 @@ async def test_client_requests( mock_http_client.request.return_value = mock_response if case.expected_exception: - with pytest.raises(case.expected_exception): - with patch("httpx.AsyncClient", return_value=mock_http_client): - if case.use_context_manager: - async with api_http_client: - await getattr(api_http_client, case.method)( - "/test-endpoint" - ) - else: + with ( + pytest.raises(case.expected_exception), + patch("httpx.AsyncClient", return_value=mock_http_client), + ): + if case.use_context_manager: + async with api_http_client: await getattr(api_http_client, case.method)("/test-endpoint") + else: + await getattr(api_http_client, case.method)("/test-endpoint") return with patch("httpx.AsyncClient", return_value=mock_http_client): diff --git a/uv.lock b/uv.lock index 7c477e3..583f8ac 100644 --- a/uv.lock +++ b/uv.lock @@ -1,90 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12.9" - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.12.13" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, - { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, - { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, - { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, - { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910, upload-time = "2025-06-14T15:14:30.604Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566, upload-time = "2025-06-14T15:14:32.275Z" }, - { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856, upload-time = "2025-06-14T15:14:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683, upload-time = "2025-06-14T15:14:36.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946, upload-time = "2025-06-14T15:14:38Z" }, - { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017, upload-time = "2025-06-14T15:14:39.951Z" }, - { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390, upload-time = "2025-06-14T15:14:42.151Z" }, - { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719, upload-time = "2025-06-14T15:14:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424, upload-time = "2025-06-14T15:14:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447, upload-time = "2025-06-14T15:14:47.911Z" }, - { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110, upload-time = "2025-06-14T15:14:50.334Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706, upload-time = "2025-06-14T15:14:52.378Z" }, - { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839, upload-time = "2025-06-14T15:14:54.617Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311, upload-time = "2025-06-14T15:14:56.597Z" }, - { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202, upload-time = "2025-06-14T15:14:58.598Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794, upload-time = "2025-06-14T15:15:00.939Z" }, - { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735, upload-time = "2025-06-14T15:15:02.858Z" }, -] - -[[package]] -name = "aiohttp-retry" -version = "2.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/61/ebda4d8e3d8cfa1fd3db0fb428db2dd7461d5742cea35178277ad180b033/aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1", size = 13608, upload-time = "2024-11-06T10:44:54.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload-time = "2024-11-06T10:44:52.917Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, -] +requires-python = ">=3.12" [[package]] name = "annotated-types" @@ -109,15 +25,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] -[[package]] -name = "attrs" -version = "25.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, -] - [[package]] name = "bandit" version = "1.9.3" @@ -156,16 +63,11 @@ name = "codesphere" version = "1.0.0" source = { editable = "." } dependencies = [ - { name = "aiohttp" }, - { name = "aiohttp-retry" }, { name = "httpx" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "python-dateutil" }, { name = "python-dotenv" }, { name = "pyyaml" }, - { name = "typing-extensions" }, - { name = "urllib3" }, ] [package.optional-dependencies] @@ -176,12 +78,11 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "ty" }, ] [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.12.13" }, - { name = "aiohttp-retry", specifier = ">=2.9.1" }, { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.2.0" }, @@ -190,12 +91,10 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.2.1" }, - { name = "python-dateutil", specifier = ">=2.9.0.post0" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.13" }, - { name = "typing-extensions", specifier = ">=4.14.0" }, - { name = "urllib3", specifier = ">=2.4.0" }, + { name = "ty", marker = "extra == 'dev'", specifier = "==0.0.58" }, ] provides-extras = ["dev"] @@ -268,66 +167,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] -[[package]] -name = "frozenlist" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, - { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, - { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, - { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, - { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, - { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, - { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, - { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, - { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, - { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, - { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, - { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, - { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, - { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -413,66 +252,6 @@ 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 = "multidict" -version = "6.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183, upload-time = "2025-05-19T14:16:37.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293, upload-time = "2025-05-19T14:14:44.724Z" }, - { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096, upload-time = "2025-05-19T14:14:45.95Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214, upload-time = "2025-05-19T14:14:47.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686, upload-time = "2025-05-19T14:14:48.366Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061, upload-time = "2025-05-19T14:14:49.952Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412, upload-time = "2025-05-19T14:14:51.812Z" }, - { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563, upload-time = "2025-05-19T14:14:53.262Z" }, - { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811, upload-time = "2025-05-19T14:14:55.232Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524, upload-time = "2025-05-19T14:14:57.226Z" }, - { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012, upload-time = "2025-05-19T14:14:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765, upload-time = "2025-05-19T14:15:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888, upload-time = "2025-05-19T14:15:01.568Z" }, - { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041, upload-time = "2025-05-19T14:15:03.759Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046, upload-time = "2025-05-19T14:15:05.698Z" }, - { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106, upload-time = "2025-05-19T14:15:07.124Z" }, - { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351, upload-time = "2025-05-19T14:15:08.556Z" }, - { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791, upload-time = "2025-05-19T14:15:09.825Z" }, - { url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123, upload-time = "2025-05-19T14:15:11.044Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049, upload-time = "2025-05-19T14:15:12.902Z" }, - { url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078, upload-time = "2025-05-19T14:15:14.282Z" }, - { url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097, upload-time = "2025-05-19T14:15:15.566Z" }, - { url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768, upload-time = "2025-05-19T14:15:17.308Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331, upload-time = "2025-05-19T14:15:18.73Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169, upload-time = "2025-05-19T14:15:20.179Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947, upload-time = "2025-05-19T14:15:21.714Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761, upload-time = "2025-05-19T14:15:23.242Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605, upload-time = "2025-05-19T14:15:24.763Z" }, - { url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144, upload-time = "2025-05-19T14:15:26.249Z" }, - { url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100, upload-time = "2025-05-19T14:15:28.303Z" }, - { url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731, upload-time = "2025-05-19T14:15:30.263Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637, upload-time = "2025-05-19T14:15:33.337Z" }, - { url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594, upload-time = "2025-05-19T14:15:34.832Z" }, - { url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359, upload-time = "2025-05-19T14:15:36.246Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903, upload-time = "2025-05-19T14:15:37.507Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895, upload-time = "2025-05-19T14:15:38.856Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183, upload-time = "2025-05-19T14:15:40.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592, upload-time = "2025-05-19T14:15:41.508Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071, upload-time = "2025-05-19T14:15:42.877Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597, upload-time = "2025-05-19T14:15:44.412Z" }, - { url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253, upload-time = "2025-05-19T14:15:46.474Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146, upload-time = "2025-05-19T14:15:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585, upload-time = "2025-05-19T14:15:49.546Z" }, - { url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080, upload-time = "2025-05-19T14:15:51.151Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558, upload-time = "2025-05-19T14:15:52.665Z" }, - { url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168, upload-time = "2025-05-19T14:15:55.279Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970, upload-time = "2025-05-19T14:15:56.806Z" }, - { url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980, upload-time = "2025-05-19T14:15:58.313Z" }, - { url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641, upload-time = "2025-05-19T14:15:59.866Z" }, - { url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728, upload-time = "2025-05-19T14:16:01.535Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913, upload-time = "2025-05-19T14:16:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112, upload-time = "2025-05-19T14:16:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" }, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -525,63 +304,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] -[[package]] -name = "propcache" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, - { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, - { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, - { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, - { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, - { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, - { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, - { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, -] - [[package]] name = "pydantic" version = "2.11.7" @@ -704,18 +426,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "python-dotenv" version = "1.2.1" @@ -789,15 +499,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, ] -[[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 = "sniffio" version = "1.3.1" @@ -816,6 +517,31 @@ 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 = "ty" +version = "0.0.58" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/4c/26c90732658903aeb1d289208f7b7b492fa21029e0c4d6c51bdd6f8f5e51/ty-0.0.58.tar.gz", hash = "sha256:8f22484174e65c630660a454bf81b80cae7a3a7e70479f19c170d6cd87949258", size = 6133665, upload-time = "2026-07-10T03:09:30.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e1/5d1aa2a75829459834689f080e4be7a9d8828ce14b939ebed69161a35811/ty-0.0.58-py3-none-linux_armv6l.whl", hash = "sha256:47412850b6fbef61c42f244f6a51aa2f2c9e91f08cfbafd2d1e3730d2419d317", size = 11706915, upload-time = "2026-07-10T03:08:51.028Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/929eda9cc72a9afe39a03c76f946a503508d37343cc8ff2e64226afda105/ty-0.0.58-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79deb7bb4e5b3a1eee6ab9abc724d6ce3559d4977982707f310a139ee11fc703", size = 11532079, upload-time = "2026-07-10T03:08:53.771Z" }, + { url = "https://files.pythonhosted.org/packages/07/43/ebc58b3fc7d86a7abba2829f1674f7d4ae3a08f9794c1f31b707950c871f/ty-0.0.58-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a28af3187e661708a386d44a4fc32896a5f589fb07b734a11ab2f516e7572b7", size = 11092983, upload-time = "2026-07-10T03:08:56.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/6e/9547dbb8e51e47749cfb721a02b4fc862f9a932fa0f66a34a4d6dc429bb1/ty-0.0.58-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21a6977e34bc362fb378add46e59d5d56331c1c36727e6904767217ca8479718", size = 11490492, upload-time = "2026-07-10T03:08:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/9f83b51b5e7795d6c8d76b4bb1bb7cebf4c10d609e846c02ab138e404556/ty-0.0.58-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ac23e6bf6105ceca46632debf1b10b98125aaf60202aeae02f6abfeea242b3d", size = 11503696, upload-time = "2026-07-10T03:09:00.741Z" }, + { url = "https://files.pythonhosted.org/packages/47/9d/9fd48a0696c680f74e50f31cd54e524eeb58c97ea9bb1c3b8e04230ba215/ty-0.0.58-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb6df6a8c6a21894807a49851370ec7fc64aa910296c78ada31db0ef19359112", size = 12158653, upload-time = "2026-07-10T03:09:02.998Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ea/b5de845d2d8edae04d901c7585af7f04a057e18b26311f7fa4ca62b2da30/ty-0.0.58-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9df5847eebc026cde088b44420a03f7c6c169a7db747176bdf0a656eb1144713", size = 12723019, upload-time = "2026-07-10T03:09:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/17/1c/54083b23eeff1e101f50b6df6a2c7f1e14b31abe0577c91bcae9e2c9395d/ty-0.0.58-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53a331a7f1f85872c810676a0f16096ef98c1b95c8c9a573fe7fde64d0a93e7c", size = 12275715, upload-time = "2026-07-10T03:09:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/22/88/16925434b06faa49d36aa7e7508a6821ec6feacb429ca2fd80a3d52716b4/ty-0.0.58-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0b05cd479fdcedc2e6781d376ee1a33569f37ae7f58357004635f615c4374c", size = 12033075, upload-time = "2026-07-10T03:09:10.222Z" }, + { url = "https://files.pythonhosted.org/packages/58/2b/b55708dd483982ae03d14da3667e3f0346cd384e11cca3dd3674a8b598c1/ty-0.0.58-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a0012786077e5becbb6add9fe51eda1d4d36d249afd3ae6cd141d554af15ae3", size = 12367729, upload-time = "2026-07-10T03:09:12.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/a3/99ad66652956408f7e9ac3db6b4a416199d773b6c073cf95b0eea126d340/ty-0.0.58-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4ede96d7f6d149156da254e784b062b817736b68d4c6d660555a3d02d96966fb", size = 11439798, upload-time = "2026-07-10T03:09:14.518Z" }, + { url = "https://files.pythonhosted.org/packages/ee/23/344ceed4fe02ed498711e1f4a47b6e311fb1e9c4fecc19d31894be7a3472/ty-0.0.58-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:47608e58f73901b989402e6f283249cda4c2314282ec368aa74ab61761c62bd5", size = 11512695, upload-time = "2026-07-10T03:09:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/55/cf/3801831812c468f3fd0b3043a80f557a9aa90e6c27375763d7c3121e03f2/ty-0.0.58-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9757de17cc17e4c6bc26e18d4e26dea52ffee53d41a10d9087c6465e6ab12e2b", size = 11812253, upload-time = "2026-07-10T03:09:19.151Z" }, + { url = "https://files.pythonhosted.org/packages/7b/80/bce4f245787b77d1ec9feec7d9161eade5e01a77dbc132e016b24df83b0d/ty-0.0.58-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f3776d54c1d935fcb8f814bd58efba402c86c555f93e1144c59d087e7aa8b906", size = 12123918, upload-time = "2026-07-10T03:09:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/bab3d6268e7e88c792bf7cdde81bd11b31aa587eaba75196502b729747c8/ty-0.0.58-py3-none-win32.whl", hash = "sha256:8f50ec0ac3b42baa4c75895dc367071dc86b00ab440d29fdc72c633286a94815", size = 11230897, upload-time = "2026-07-10T03:09:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/9c/68/d9504c895864aaa84a840dce6ac7f8e681f6938be1882c8d4f60832dbe57/ty-0.0.58-py3-none-win_amd64.whl", hash = "sha256:de8847b3a65475ae4773bddd3126bfcf29f017e88967c4dcc9c75d743a4d3e5c", size = 12299376, upload-time = "2026-07-10T03:09:26.292Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c9847cb680b5fe8e1f7d7b483edd5cedcc29394496e7b8ed40d96be796ba/ty-0.0.58-py3-none-win_arm64.whl", hash = "sha256:7334bb38789878f60677f2eb9c1de4bfdf4583e2443790989c77da9b10fe0989", size = 11710495, upload-time = "2026-07-10T03:09:28.478Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.0" @@ -837,15 +563,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] -[[package]] -name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, -] - [[package]] name = "virtualenv" version = "20.31.2" @@ -859,68 +576,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] - -[[package]] -name = "yarl" -version = "1.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, -]