From 9838d2a4593d76c67b5fa339c36fd5beb1281574 Mon Sep 17 00:00:00 2001 From: igalshilman Date: Tue, 11 Nov 2025 09:57:46 +0100 Subject: [PATCH] Add ruff and pyright for linting and type-checks --- .github/workflows/test.yml | 32 +- Justfile | 53 +- examples/concurrent_greeter.py | 9 +- examples/example.py | 8 +- examples/greeter.py | 1 + examples/pydantic_greeter.py | 4 + examples/random_greeter.py | 15 +- examples/virtual_object.py | 2 + examples/workflow.py | 10 +- pyproject.toml | 20 +- python/restate/__init__.py | 47 +- python/restate/asyncio.py | 24 +- python/restate/aws_lambda.py | 87 +- python/restate/context.py | 307 +++---- python/restate/discovery.py | 277 +++--- python/restate/endpoint.py | 8 +- python/restate/exceptions.py | 22 +- python/restate/handler.py | 96 ++- python/restate/harness.py | 163 +++- python/restate/logging.py | 3 + python/restate/object.py | 91 +- python/restate/retry_policy.py | 2 + python/restate/serde.py | 77 +- python/restate/server.py | 184 ++-- python/restate/server_context.py | 485 +++++++---- python/restate/server_types.py | 65 +- python/restate/service.py | 85 +- python/restate/types.py | 29 + python/restate/vm.py | 110 ++- python/restate/workflow.py | 222 ++--- shell.darwin.nix | 35 + shell.nix | 35 - test-services/Dockerfile | 15 +- test-services/requirements.txt | 2 - test-services/services/__init__.py | 14 +- test-services/services/awakeable_holder.py | 3 + .../services/block_and_wait_workflow.py | 2 + test-services/services/cancel_test.py | 6 +- test-services/services/counter.py | 2 +- test-services/services/failing.py | 26 +- test-services/services/interpreter.py | 68 +- test-services/services/kill_test.py | 4 + test-services/services/list_object.py | 3 + test-services/services/map_object.py | 7 +- test-services/services/non_determinism.py | 7 + test-services/services/proxy.py | 57 +- test-services/services/test_utils.py | 14 +- .../virtual_object_command_interpreter.py | 90 +- test-services/testservices.py | 15 +- tests/serde.py | 3 +- uv.lock | 804 ++++++++++++++++++ 51 files changed, 2584 insertions(+), 1166 deletions(-) create mode 100644 python/restate/types.py create mode 100755 shell.darwin.nix delete mode 100755 shell.nix delete mode 100644 test-services/requirements.txt create mode 100644 uv.lock diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5817062..8f430b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,17 +1,3 @@ -name: Test - -on: - pull_request: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: read - checks: write - pull-requests: write - jobs: lint-and-test: name: "Lint and Test (Python ${{ matrix.python }})" @@ -19,19 +5,17 @@ jobs: strategy: matrix: python: [ "3.10", "3.11", "3.12", "3.13" ] + env: + UV_PYTHON: ${{ matrix.python }} steps: - uses: actions/checkout@v4 - uses: extractions/setup-just@v2 - - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v5 with: - python-version: ${{ matrix.python }} - - name: Build Rust module - uses: PyO3/maturin-action@v1 - with: - args: --out dist --interpreter python${{ matrix.python }} - sccache: 'true' - container: off - - run: pip install -r requirements.txt - - run: pip install dist/* + enable-cache: true + - name: Install dependencies + run: just sync + - name: Build + run: just build - name: Verify run: just verify diff --git a/Justfile b/Justfile index 5e3de30..93c3702 100644 --- a/Justfile +++ b/Justfile @@ -1,38 +1,47 @@ -# Justfile +# Use UV_PYTHON env variable to select either a python version or +# the complete python to your python interpreter -python := "python3" +default := "all" -default: - @echo "Available recipes:" - @echo " mypy - Run mypy for type checking" - @echo " pylint - Run pylint for linting" - @echo " test - Run pytest for testing" - @echo " verify - Run mypy, pylint, test" +set shell := ["bash", "-c"] -# Recipe to run mypy for type checking -mypy: - @echo "Running mypy..." - {{python}} -m mypy --check-untyped-defs --ignore-missing-imports python/restate/ - {{python}} -m mypy --check-untyped-defs --ignore-missing-imports examples/ +sync: + uv sync --all-extras --all-packages -# Recipe to run pylint for linting -pylint: - @echo "Running pylint..." - {{python}} -m pylint python/restate --ignore-paths='^.*.?venv.*$' - {{python}} -m pylint examples/ --ignore-paths='^.*\.?venv.*$' +format: + uv run ruff format + uv run ruff check --fix --fix-only + +lint: + uv run ruff format --check + uv run ruff check + +typecheck-pyright: + PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright python/ + PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright examples/ + PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright tests + PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright test-services/ + +typecheck-mypy: + uv run -m mypy --check-untyped-defs --ignore-missing-imports python/ + uv run -m mypy --check-untyped-defs --ignore-missing-imports examples/ + uv run -m mypy --check-untyped-defs --ignore-missing-imports tests/ + +typecheck: typecheck-pyright typecheck-mypy test: - @echo "Running Python tests..." - {{python}} -m pytest tests/* + uv run -m pytest tests/* + # Recipe to run both mypy and pylint -verify: mypy pylint test +verify: format lint typecheck test @echo "Type checking and linting completed successfully." # Recipe to build the project build: @echo "Building the project..." - maturin build --release + #maturin build --release + uv build --all-packages clean: @echo "Cleaning the project" diff --git a/examples/concurrent_greeter.py b/examples/concurrent_greeter.py index 3bd8c8d..cdd16fc 100644 --- a/examples/concurrent_greeter.py +++ b/examples/concurrent_greeter.py @@ -20,19 +20,24 @@ from pydantic import BaseModel from restate import wait_completed, Service, Context + # models class GreetingRequest(BaseModel): name: str + class Greeting(BaseModel): messages: typing.List[str] + class Message(BaseModel): role: str content: str + concurrent_greeter = Service("concurrent_greeter") + @concurrent_greeter.handler() async def greet(ctx: Context, req: GreetingRequest) -> Greeting: claude = ctx.service_call(claude_sonnet, arg=Message(role="user", content=f"please greet {req.name}")) @@ -45,17 +50,19 @@ async def greet(ctx: Context, req: GreetingRequest) -> Greeting: # cancel the pending calls for f in pending: - await f.cancel_invocation() # type: ignore + await f.cancel_invocation() # type: ignore return Greeting(messages=greetings) # not really interesting, just for this demo: + @concurrent_greeter.handler() async def claude_sonnet(ctx: Context, req: Message) -> str: return f"Bonjour {req.content[13:]}!" + @concurrent_greeter.handler() async def open_ai(ctx: Context, req: Message) -> str: return f"Hello {req.content[13:]}!" diff --git a/examples/example.py b/examples/example.py index e19c769..51224ee 100644 --- a/examples/example.py +++ b/examples/example.py @@ -24,17 +24,13 @@ logging.basicConfig(level=logging.INFO) -app = restate.app(services=[greeter, - random_greeter, - counter, - payment, - pydantic_greeter, - concurrent_greeter]) +app = restate.app(services=[greeter, random_greeter, counter, payment, pydantic_greeter, concurrent_greeter]) if __name__ == "__main__": import hypercorn import hypercorn.asyncio import asyncio + conf = hypercorn.Config() conf.bind = ["0.0.0.0:9080"] asyncio.run(hypercorn.asyncio.serve(app, conf)) diff --git a/examples/greeter.py b/examples/greeter.py index 2d1af12..5b81117 100644 --- a/examples/greeter.py +++ b/examples/greeter.py @@ -21,6 +21,7 @@ greeter = Service("greeter") + @greeter.handler() async def greet(ctx: Context, name: str) -> str: logger.info("Received greeting request: %s", name) diff --git a/examples/pydantic_greeter.py b/examples/pydantic_greeter.py index 3b064a0..02efd18 100644 --- a/examples/pydantic_greeter.py +++ b/examples/pydantic_greeter.py @@ -17,17 +17,21 @@ from pydantic import BaseModel from restate import Service, Context + # models class GreetingRequest(BaseModel): name: str + class Greeting(BaseModel): message: str + # service pydantic_greeter = Service("pydantic_greeter") + @pydantic_greeter.handler() async def greet(ctx: Context, req: GreetingRequest) -> Greeting: return Greeting(message=f"Hello {req.name}!") diff --git a/examples/random_greeter.py b/examples/random_greeter.py index 2cafbcb..02a4267 100644 --- a/examples/random_greeter.py +++ b/examples/random_greeter.py @@ -9,6 +9,7 @@ # https://github.com/restatedev/sdk-typescript/blob/main/LICENSE # """example.py""" + from datetime import datetime # pylint: disable=C0116 @@ -18,9 +19,9 @@ random_greeter = Service("random_greeter") + @random_greeter.handler() async def greet(ctx: Context, name: str) -> str: - # ctx.random() returns a Python Random instance seeded deterministically. # By using ctx.random() you don't write entries in the journal, # but you still get the same generated values on retries. @@ -48,8 +49,10 @@ async def greet(ctx: Context, name: str) -> str: # end = await ctx.time() # delta = datetime.timedelta(seconds=(end-start)) - return (f"Hello {name} with " - f"random number {random_number}, " - f"random bytes {random_bytes!r} " - f"random uuid {random_uuid}," - f"now datetime {now_datetime}!") + return ( + f"Hello {name} with " + f"random number {random_number}, " + f"random bytes {random_bytes!r} " + f"random uuid {random_uuid}," + f"now datetime {now_datetime}!" + ) diff --git a/examples/virtual_object.py b/examples/virtual_object.py index 15127f3..023a982 100644 --- a/examples/virtual_object.py +++ b/examples/virtual_object.py @@ -16,6 +16,7 @@ counter = VirtualObject("counter") + @counter.handler() async def increment(ctx: ObjectContext, value: int) -> int: n = await ctx.get("counter", type_hint=int) or 0 @@ -23,6 +24,7 @@ async def increment(ctx: ObjectContext, value: int) -> int: ctx.set("counter", n) return n + @counter.handler(kind="shared") async def count(ctx: ObjectSharedContext) -> int: return await ctx.get("counter") or 0 diff --git a/examples/workflow.py b/examples/workflow.py index aae3cc9..1e1de63 100644 --- a/examples/workflow.py +++ b/examples/workflow.py @@ -23,6 +23,7 @@ payment = Workflow("payment") + @payment.main() async def pay(ctx: WorkflowContext, amount: int): workflow_key = ctx.key() @@ -44,16 +45,17 @@ def payment_gateway(): # Wait for the payment to be verified match await select(result=ctx.promise("verify.payment").value(), timeout=ctx.sleep(TIMEOUT)): - case ['result', "approved"]: + case ["result", "approved"]: ctx.set("status", "payment approved") - return { "success" : True } - case ['result', "declined"]: + return {"success": True} + case ["result", "declined"]: ctx.set("status", "payment declined") raise TerminalError(message="Payment declined", status_code=401) - case ['timeout', _]: + case ["timeout", _]: ctx.set("status", "payment verification timed out") raise TerminalError(message="Payment verification timed out", status_code=410) + @payment.handler() async def payment_verified(ctx: WorkflowSharedContext, result: str): promise = ctx.promise("verify.payment", type_hint=str) diff --git a/pyproject.toml b/pyproject.toml index 79d77cb..86140ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ Source = "https://github.com/restatedev/sdk-python" [project.optional-dependencies] test = ["pytest", "hypercorn"] -lint = ["mypy", "pylint"] +lint = ["mypy>=1.11.2", "pyright>=1.1.390", "ruff>=0.6.9"] harness = ["testcontainers", "hypercorn", "httpx"] serde = ["dacite", "pydantic"] @@ -34,3 +34,21 @@ build-backend = "maturin" features = ["pyo3/extension-module"] module-name = "restate._internal" python-source = "python" + +[tool.ruff] +line-length = 120 +target-version = "py310" +include = [ + "python/**/*.py", + "examples/**/*.py", + "tests/**/*.py", + "test-services//**/*.py", +] + +[tool.ruff.lint] +ignore = ["E741"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning", +] diff --git a/python/restate/__init__.py b/python/restate/__init__.py index b6f7231..8850752 100644 --- a/python/restate/__init__.py +++ b/python/restate/__init__.py @@ -12,6 +12,12 @@ Restate SDK for Python """ +from contextlib import contextmanager +import typing + +from restate.server_types import RestateAppT +from restate.types import TestHarnessEnvironment + from .service import Service from .object import VirtualObject from .workflow import Workflow @@ -20,8 +26,16 @@ from .context import Context, ObjectContext, ObjectSharedContext from .context import WorkflowContext, WorkflowSharedContext from .retry_policy import InvocationRetryPolicy + # pylint: disable=line-too-long -from .context import DurablePromise, RestateDurableFuture, RestateDurableCallFuture, RestateDurableSleepFuture, SendHandle, RunOptions +from .context import ( + DurablePromise, + RestateDurableFuture, + RestateDurableCallFuture, + RestateDurableSleepFuture, + SendHandle, + RunOptions, +) from .exceptions import TerminalError, SdkInternalBaseException, is_internal_exception from .asyncio import as_completed, gather, wait_completed, select @@ -29,20 +43,35 @@ from .logging import getLogger, RestateLoggingFilter + try: - from .harness import test_harness # type: ignore + from .harness import create_test_harness, test_harness # type: ignore except ImportError: # we don't have the appropriate dependencies installed # pylint: disable=unused-argument, redefined-outer-name - def test_harness(app, # type: ignore - follow_logs: bool = False, - restate_image: str = "", - always_replay: bool = False, - disable_retries: bool = False): + @contextmanager + def create_test_harness( + app: RestateAppT, + follow_logs: bool = False, + restate_image: str = "restatedev/restate:latest", + always_replay: bool = False, + disable_retries: bool = False, + ) -> typing.Generator[TestHarnessEnvironment, None, None]: """a dummy harness constructor to raise ImportError""" raise ImportError("Install restate-sdk[harness] to use this feature") + def test_harness( + app: RestateAppT, + follow_logs: bool = False, + restate_image: str = "restatedev/restate:latest", + always_replay: bool = False, + disable_retries: bool = False, + ): + """a dummy harness constructor to raise ImportError""" + raise ImportError("Install restate-sdk[harness] to use this feature") + + __all__ = [ "Service", "VirtualObject", @@ -60,6 +89,7 @@ def test_harness(app, # type: ignore "RunOptions", "TerminalError", "app", + "create_test_harness", "test_harness", "gather", "as_completed", @@ -69,5 +99,6 @@ def test_harness(app, # type: ignore "RestateLoggingFilter", "InvocationRetryPolicy", "SdkInternalBaseException", - "is_internal_exception" + "is_internal_exception", + "getLogger", ] diff --git a/python/restate/asyncio.py b/python/restate/asyncio.py index 9417134..7c8d18f 100644 --- a/python/restate/asyncio.py +++ b/python/restate/asyncio.py @@ -17,6 +17,7 @@ from restate.context import RestateDurableFuture from restate.server_context import ServerDurableFuture, ServerInvocationContext + async def gather(*futures: RestateDurableFuture[Any]) -> List[RestateDurableFuture[Any]]: """ Blocks until all futures are completed. @@ -27,6 +28,7 @@ async def gather(*futures: RestateDurableFuture[Any]) -> List[RestateDurableFutu pass return list(futures) + async def select(**kws: RestateDurableFuture[Any]) -> List[Any]: """ Blocks until one of the futures is completed. @@ -50,20 +52,21 @@ async def select(**kws: RestateDurableFuture[Any]) -> List[Any]: raise TerminalError(message="Payment declined", status_code=401) case ['timeout', _]: raise TerminalError(message="Payment verification timed out", status_code=410) - + """ if not kws: raise ValueError("At least one future must be passed.") - reverse = { f: key for key, f in kws.items() } + reverse = {f: key for key, f in kws.items()} async for f in as_completed(*kws.values()): return [reverse[f], await f] assert False, "unreachable" + async def as_completed(*futures: RestateDurableFuture[Any]): """ Returns an iterator that yields the futures as they are completed. - - example: + + example: async for future in as_completed(f1, f2, f3): # do something with the completed future @@ -77,7 +80,10 @@ async def as_completed(*futures: RestateDurableFuture[Any]): yield f remaining = waiting -async def wait_completed(*args: RestateDurableFuture[Any]) -> Tuple[List[RestateDurableFuture[Any]], List[RestateDurableFuture[Any]]]: + +async def wait_completed( + *args: RestateDurableFuture[Any], +) -> Tuple[List[RestateDurableFuture[Any]], List[RestateDurableFuture[Any]]]: """ Blocks until at least one of the futures is completed. @@ -107,7 +113,7 @@ async def wait_completed(*args: RestateDurableFuture[Any]) -> Tuple[List[Restate if completed: # the user had passed some completed futures, so we can return them immediately - return completed, uncompleted # type: ignore + return completed, uncompleted # type: ignore completed = [] uncompleted = [] @@ -117,7 +123,7 @@ async def wait_completed(*args: RestateDurableFuture[Any]) -> Tuple[List[Restate for index, handle in enumerate(handles): future = futures[index] if context.vm.is_completed(handle): - completed.append(future) # type: ignore + completed.append(future) # type: ignore else: - uncompleted.append(future) # type: ignore - return completed, uncompleted # type: ignore + uncompleted.append(future) # type: ignore + return completed, uncompleted # type: ignore diff --git a/python/restate/aws_lambda.py b/python/restate/aws_lambda.py index 48edc28..aa335d9 100644 --- a/python/restate/aws_lambda.py +++ b/python/restate/aws_lambda.py @@ -11,51 +11,30 @@ """ This module contains the Lambda/ASGI adapter. """ + import asyncio import base64 import os -from typing import TypedDict, Dict, cast, Union, Any, Callable - -from restate.server_types import (ASGIApp, - Scope, - Receive, - HTTPResponseStartEvent, - HTTPResponseBodyEvent, - HTTPRequestEvent) - -class RestateLambdaRequest(TypedDict): - """ - Restate Lambda request - - :see: https://github.com/restatedev/restate/blob/1a10c05b16b387191060b49faffb0335ee97e96d/crates/service-client/src/lambda.rs#L297 # pylint: disable=line-too-long - """ - path: str - httpMethod: str - headers: Dict[str, str] - body: str - isBase64Encoded: bool - - -class RestateLambdaResponse(TypedDict): - """ - Restate Lambda response - - :see: https://github.com/restatedev/restate/blob/1a10c05b16b387191060b49faffb0335ee97e96d/crates/service-client/src/lambda.rs#L310 # pylint: disable=line-too-long - """ - statusCode: int - headers: Dict[str, str] - body: str - isBase64Encoded: bool +from typing import cast, Union, Any - -RestateLambdaHandler = Callable[[RestateLambdaRequest, Any], RestateLambdaResponse] +from restate.server_types import ( + ASGIApp, + Receive, + RestateLambdaHandler, + Scope, + HTTPResponseStartEvent, + HTTPResponseBodyEvent, + HTTPRequestEvent, + RestateLambdaRequest, + RestateLambdaResponse, +) def create_scope(req: RestateLambdaRequest) -> Scope: """ Create ASGI scope from lambda request """ - headers = {k.lower(): v for k, v in req.get('headers', {}).items()} + headers = {k.lower(): v for k, v in req.get("headers", {}).items()} http_method = req["httpMethod"] path = req["path"] @@ -69,10 +48,10 @@ def create_scope(req: RestateLambdaRequest) -> Scope: "asgi": {"version": "3.0", "spec_version": "2.0"}, "raw_path": path.encode(), "root_path": "", - "query_string": b'', + "query_string": b"", "client": None, "server": None, - "extensions": None + "extensions": None, } @@ -80,19 +59,16 @@ def request_to_receive(req: RestateLambdaRequest) -> Receive: """ Create ASGI Receive from lambda request """ - assert req['isBase64Encoded'] - body = base64.b64decode(req['body']) - - events = cast(list[HTTPRequestEvent], [{ - "type": "http.request", - "body": body, - "more_body": False - }, - { - "type": "http.request", - "body": b'', - "more_body": False - }]) + assert req["isBase64Encoded"] + body = base64.b64decode(req["body"]) + + events = cast( + list[HTTPRequestEvent], + [ + {"type": "http.request", "body": body, "more_body": False}, + {"type": "http.request", "body": b"", "more_body": False}, + ], + ) async def recv() -> HTTPRequestEvent: if len(events) != 0: @@ -108,6 +84,7 @@ class ResponseCollector: """ Response collector from ASGI Send to Lambda """ + def __init__(self): self.body = bytearray() self.headers = {} @@ -119,10 +96,7 @@ async def __call__(self, message: Union[HTTPResponseStartEvent, HTTPResponseBody """ if message["type"] == "http.response.start": self.status_code = cast(int, message["status"]) - self.headers = { - key.decode("utf-8"): value.decode("utf-8") - for key, value in message["headers"] - } + self.headers = {key.decode("utf-8"): value.decode("utf-8") for key, value in message["headers"]} elif message["type"] == "http.response.body" and "body" in message: self.body.extend(message["body"]) return @@ -135,7 +109,7 @@ def to_lambda_response(self) -> RestateLambdaResponse: "statusCode": self.status_code, "headers": self.headers, "isBase64Encoded": True, - "body": base64.b64encode(self.body).decode() + "body": base64.b64encode(self.body).decode(), } @@ -147,8 +121,7 @@ def is_running_on_lambda() -> bool: return "AWS_LAMBDA_FUNCTION_NAME" in os.environ -def wrap_asgi_as_lambda_handler(asgi_app: ASGIApp) \ - -> Callable[[RestateLambdaRequest, Any], RestateLambdaResponse]: +def wrap_asgi_as_lambda_handler(asgi_app: ASGIApp) -> RestateLambdaHandler: """ Wrap the given asgi_app in a Lambda handler """ diff --git a/python/restate/context.py b/python/restate/context.py index 41de608..2af7a6c 100644 --- a/python/restate/context.py +++ b/python/restate/context.py @@ -23,14 +23,15 @@ from restate.serde import DefaultSerde, Serde -T = TypeVar('T') -I = TypeVar('I') -O = TypeVar('O') -P = ParamSpec('P') +T = TypeVar("T") +I = TypeVar("I") +O = TypeVar("O") +P = ParamSpec("P") HandlerType = Union[Callable[[Any, I], Awaitable[O]], Callable[[Any], Awaitable[O]]] RunAction = Union[Callable[..., Coroutine[Any, Any, T]], Callable[..., T]] + # pylint: disable=R0902 @dataclass class RunOptions(typing.Generic[T]): @@ -68,6 +69,7 @@ class RunOptions(typing.Generic[T]): max_retry_duration: Optional[timedelta] = None """Deprecated: Use max_duration instead.""" + # pylint: disable=R0903 class RestateDurableFuture(typing.Generic[T], Awaitable[T]): """ @@ -79,7 +81,6 @@ def __await__(self) -> typing.Generator[Any, Any, T]: pass - # pylint: disable=R0903 class RestateDurableCallFuture(RestateDurableFuture[T]): """ @@ -113,6 +114,7 @@ class RestateDurableSleepFuture(RestateDurableFuture[None]): def __await__(self) -> typing.Generator[Any, Any, None]: pass + class AttemptFinishedEvent(abc.ABC): """ Represents an attempt finished event. @@ -131,7 +133,6 @@ def is_set(self) -> bool: Returns True if the event is set, False otherwise. """ - @abc.abstractmethod async def wait(self): """ @@ -151,9 +152,10 @@ class Request: body (bytes): The body of the request. attempt_finished_event (AttemptFinishedEvent): The teardown event of the request. """ + id: str headers: Dict[str, str] - attempt_headers: Dict[str,str] + attempt_headers: Dict[str, str] body: bytes attempt_finished_event: AttemptFinishedEvent @@ -169,11 +171,9 @@ class KeyValueStore(abc.ABC): """ @abc.abstractmethod - def get(self, - name: str, - serde: Serde[T] = DefaultSerde(), - type_hint: Optional[typing.Type[T]] = None - ) -> Awaitable[Optional[T]]: + def get( + self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> Awaitable[Optional[T]]: """ Retrieves the value associated with the given name. @@ -189,10 +189,7 @@ def state_keys(self) -> Awaitable[List[str]]: """Returns the list of keys in the store.""" @abc.abstractmethod - def set(self, - name: str, - value: T, - serde: Serde[T] = DefaultSerde()) -> None: + def set(self, name: str, value: T, serde: Serde[T] = DefaultSerde()) -> None: """set the value associated with the given name.""" @abc.abstractmethod @@ -203,6 +200,7 @@ def clear(self, name: str) -> None: def clear_all(self) -> None: """clear all the values in the store.""" + # pylint: disable=R0903 class SendHandle(abc.ABC): """ @@ -264,15 +262,16 @@ def time(self) -> RestateDurableFuture[float]: @overload @abc.abstractmethod - def run(self, - name: str, - action: Callable[..., Coroutine[Any, Any,T]], - serde: Serde[T] = DefaultSerde(), - max_attempts: typing.Optional[int] = None, - max_retry_duration: typing.Optional[timedelta] = None, - type_hint: Optional[typing.Type[T]] = None, - args: Optional[typing.Tuple[Any, ...]] = None, - ) -> RestateDurableFuture[T]: + def run( + self, + name: str, + action: Callable[..., Coroutine[Any, Any, T]], + serde: Serde[T] = DefaultSerde(), + max_attempts: typing.Optional[int] = None, + max_retry_duration: typing.Optional[timedelta] = None, + type_hint: Optional[typing.Type[T]] = None, + args: Optional[typing.Tuple[Any, ...]] = None, + ) -> RestateDurableFuture[T]: """ Runs the given action with the given name. @@ -295,15 +294,16 @@ def run(self, @overload @abc.abstractmethod - def run(self, - name: str, - action: Callable[..., T], - serde: Serde[T] = DefaultSerde(), - max_attempts: typing.Optional[int] = None, - max_retry_duration: typing.Optional[timedelta] = None, - type_hint: Optional[typing.Type[T]] = None, - args: Optional[typing.Tuple[Any, ...]] = None, - ) -> RestateDurableFuture[T]: + def run( + self, + name: str, + action: Callable[..., T], + serde: Serde[T] = DefaultSerde(), + max_attempts: typing.Optional[int] = None, + max_retry_duration: typing.Optional[timedelta] = None, + type_hint: Optional[typing.Type[T]] = None, + args: Optional[typing.Tuple[Any, ...]] = None, + ) -> RestateDurableFuture[T]: """ Runs the given coroutine action with the given name. @@ -325,15 +325,16 @@ def run(self, """ @abc.abstractmethod - def run(self, - name: str, - action: RunAction[T], - serde: Serde[T] = DefaultSerde(), - max_attempts: typing.Optional[int] = None, - max_retry_duration: typing.Optional[timedelta] = None, - type_hint: Optional[typing.Type[T]] = None, - args: Optional[typing.Tuple[Any, ...]] = None, - ) -> RestateDurableFuture[T]: + def run( + self, + name: str, + action: RunAction[T], + serde: Serde[T] = DefaultSerde(), + max_attempts: typing.Optional[int] = None, + max_retry_duration: typing.Optional[timedelta] = None, + type_hint: Optional[typing.Type[T]] = None, + args: Optional[typing.Tuple[Any, ...]] = None, + ) -> RestateDurableFuture[T]: """ Runs the given action with the given name. @@ -354,17 +355,17 @@ def run(self, """ - @overload @abc.abstractmethod - def run_typed(self, - name: str, - action: Callable[P, Coroutine[Any, Any,T]], - options: RunOptions[T] = RunOptions(), - /, - *args: P.args, - **kwargs: P.kwargs, - ) -> RestateDurableFuture[T]: + def run_typed( + self, + name: str, + action: Callable[P, Coroutine[Any, Any, T]], + options: RunOptions[T] = RunOptions(), + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> RestateDurableFuture[T]: """ Typed version of run that provides type hints for the function arguments. Runs the given action with the given name. @@ -379,14 +380,15 @@ def run_typed(self, @overload @abc.abstractmethod - def run_typed(self, - name: str, - action: Callable[P, T], - options: RunOptions[T] = RunOptions(), - /, - *args: P.args, - **kwargs: P.kwargs, - ) -> RestateDurableFuture[T]: + def run_typed( + self, + name: str, + action: Callable[P, T], + options: RunOptions[T] = RunOptions(), + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> RestateDurableFuture[T]: """ Typed version of run that provides type hints for the function arguments. Runs the given coroutine action with the given name. @@ -400,14 +402,15 @@ def run_typed(self, """ @abc.abstractmethod - def run_typed(self, - name: str, - action: Union[Callable[P, Coroutine[Any, Any, T]], Callable[P, T]], - options: RunOptions[T] = RunOptions(), - /, - *args: P.args, - **kwargs: P.kwargs, - ) -> RestateDurableFuture[T]: + def run_typed( + self, + name: str, + action: Union[Callable[P, Coroutine[Any, Any, T]], Callable[P, T]], + options: RunOptions[T] = RunOptions(), + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> RestateDurableFuture[T]: """ Typed version of run that provides type hints for the function arguments. Runs the given action with the given name. @@ -428,121 +431,124 @@ def sleep(self, delta: timedelta, name: Optional[str] = None) -> RestateDurableS """ @abc.abstractmethod - def service_call(self, - tpe: HandlerType[I, O], - arg: I, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O]: + def service_call( + self, + tpe: HandlerType[I, O], + arg: I, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O]: """ Invokes the given service with the given argument. """ - @abc.abstractmethod - def service_send(self, - tpe: HandlerType[I, O], - arg: I, - send_delay: Optional[timedelta] = None, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> SendHandle: + def service_send( + self, + tpe: HandlerType[I, O], + arg: I, + send_delay: Optional[timedelta] = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: """ Invokes the given service with the given argument. """ @abc.abstractmethod - def object_call(self, - tpe: HandlerType[I, O], - key: str, - arg: I, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O]: + def object_call( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O]: """ Invokes the given object with the given argument. """ @abc.abstractmethod - def object_send(self, - tpe: HandlerType[I, O], - key: str, - arg: I, - send_delay: Optional[timedelta] = None, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> SendHandle: + def object_send( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + send_delay: Optional[timedelta] = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: """ Send a message to an object with the given argument. """ @abc.abstractmethod - def workflow_call(self, - tpe: HandlerType[I, O], - key: str, - arg: I, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O]: + def workflow_call( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O]: """ Invokes the given workflow with the given argument. """ @abc.abstractmethod - def workflow_send(self, - tpe: HandlerType[I, O], - key: str, - arg: I, - send_delay: Optional[timedelta] = None, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> SendHandle: + def workflow_send( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + send_delay: Optional[timedelta] = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: """ Send a message to an object with the given argument. """ # pylint: disable=R0913 @abc.abstractmethod - def generic_call(self, - service: str, - handler: str, - arg: bytes, - key: Optional[str] = None, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[bytes]: + def generic_call( + self, + service: str, + handler: str, + arg: bytes, + key: Optional[str] = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[bytes]: """ Invokes the given generic service/handler with the given argument. """ @abc.abstractmethod - def generic_send(self, - service: str, - handler: str, - arg: bytes, - key: Optional[str] = None, - send_delay: Optional[timedelta] = None, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> SendHandle: + def generic_send( + self, + service: str, + handler: str, + arg: bytes, + key: Optional[str] = None, + send_delay: Optional[timedelta] = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: """ Send a message to a generic service/handler with the given argument. """ @abc.abstractmethod - def awakeable(self, - serde: Serde[T] = DefaultSerde(), - type_hint: Optional[typing.Type[T]] = None - ) -> typing.Tuple[str, RestateDurableFuture[T]]: + def awakeable( + self, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> typing.Tuple[str, RestateDurableFuture[T]]: """ Returns the name of the awakeable and the future to be awaited. """ @abc.abstractmethod - def resolve_awakeable(self, - name: str, - value: I, - serde: Serde[I] = DefaultSerde()) -> None: + def resolve_awakeable(self, name: str, value: I, serde: Serde[I] = DefaultSerde()) -> None: """ Resolves the awakeable with the given name. """ @@ -560,9 +566,9 @@ def cancel_invocation(self, invocation_id: str): """ @abc.abstractmethod - def attach_invocation(self, invocation_id: str, serde: Serde[T] = DefaultSerde(), - type_hint: typing.Optional[typing.Type[T]] = None - ) -> RestateDurableFuture[T]: + def attach_invocation( + self, invocation_id: str, serde: Serde[T] = DefaultSerde(), type_hint: typing.Optional[typing.Type[T]] = None + ) -> RestateDurableFuture[T]: """ Attaches the invocation with the given id. """ @@ -590,11 +596,9 @@ def key(self) -> str: """Returns the key of the current object.""" @abc.abstractmethod - def get(self, - name: str, - serde: Serde[T] = DefaultSerde(), - type_hint: Optional[typing.Type[T]] = None - ) -> RestateDurableFuture[Optional[T]]: + def get( + self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> RestateDurableFuture[Optional[T]]: """ Retrieves the value associated with the given name. @@ -611,6 +615,7 @@ def state_keys(self) -> Awaitable[List[str]]: Returns the list of keys in the store. """ + class DurablePromise(typing.Generic[T]): """ Represents a durable promise. @@ -647,27 +652,33 @@ def value(self) -> RestateDurableFuture[T]: @abc.abstractmethod def __await__(self) -> typing.Generator[Any, Any, T]: """ - Returns the value of the promise. This is a shortcut for calling value() and awaiting it. + Returns the value of the promise. This is a shortcut for calling value() and awaiting it. """ + class WorkflowContext(ObjectContext): """ Represents the context of the current workflow invocation. """ @abc.abstractmethod - def promise(self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None) -> DurablePromise[T]: + def promise( + self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> DurablePromise[T]: """ Returns a durable promise with the given name. """ + class WorkflowSharedContext(ObjectSharedContext): """ Represents the context of the current workflow invocation. """ @abc.abstractmethod - def promise(self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None) -> DurablePromise[T]: + def promise( + self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> DurablePromise[T]: """ Returns a durable promise with the given name. """ diff --git a/python/restate/discovery.py b/python/restate/discovery.py index bcae0c4..7fef373 100644 --- a/python/restate/discovery.py +++ b/python/restate/discovery.py @@ -10,8 +10,8 @@ # """ Holds the discovery API objects as defined by the restate protocol. -Note that the classes defined here do not use snake case, because they -are intended to be serialized to JSON, and their cases must remain in the +Note that the classes defined here do not use snake case, because they +are intended to be serialized to JSON, and their cases must remain in the case that the restate server understands. """ @@ -32,54 +32,66 @@ from restate.endpoint import Endpoint as RestateEndpoint from restate.handler import TypeHint +from restate.object import VirtualObject +from restate.workflow import Workflow + class ProtocolMode(Enum): BIDI_STREAM = "BIDI_STREAM" REQUEST_RESPONSE = "REQUEST_RESPONSE" + class ServiceType(Enum): VIRTUAL_OBJECT = "VIRTUAL_OBJECT" SERVICE = "SERVICE" WORKFLOW = "WORKFLOW" + class ServiceHandlerType(Enum): WORKFLOW = "WORKFLOW" EXCLUSIVE = "EXCLUSIVE" SHARED = "SHARED" + class InputPayload: def __init__(self, required: bool, contentType: str, jsonSchema: Optional[Any] = None): self.required = required self.contentType = contentType self.jsonSchema = jsonSchema + class OutputPayload: - def __init__(self, setContentTypeIfEmpty: bool, contentType: Optional[str] = None, jsonSchema: Optional[Any] = None): + def __init__( + self, setContentTypeIfEmpty: bool, contentType: Optional[str] = None, jsonSchema: Optional[Any] = None + ): self.contentType = contentType self.setContentTypeIfEmpty = setContentTypeIfEmpty self.jsonSchema = jsonSchema + class Handler: # pylint: disable=R0902,R0914 - def __init__(self, - name: str, - ty: Optional[ServiceHandlerType] = None, - input: Optional[InputPayload | Dict[str, str]] = None, - output: Optional[OutputPayload] = None, - description: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - inactivityTimeout: Optional[int] = None, - abortTimeout: Optional[int] = None, - journalRetention: Optional[int] = None, - idempotencyRetention: Optional[int] = None, - workflowCompletionRetention: Optional[int] = None, - enableLazyState: Optional[bool] = None, - ingressPrivate: Optional[bool] = None, - retryPolicyInitialInterval: Optional[int] = None, - retryPolicyMaxInterval: Optional[int] = None, - retryPolicyMaxAttempts: Optional[int] = None, - retryPolicyExponentiationFactor: Optional[float] = None, - retryPolicyOnMaxAttempts: Optional[str] = None): + def __init__( + self, + name: str, + ty: Optional[ServiceHandlerType] = None, + input: Optional[InputPayload | Dict[str, str]] = None, + output: Optional[OutputPayload] = None, + description: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + inactivityTimeout: Optional[int] = None, + abortTimeout: Optional[int] = None, + journalRetention: Optional[int] = None, + idempotencyRetention: Optional[int] = None, + workflowCompletionRetention: Optional[int] = None, + enableLazyState: Optional[bool] = None, + ingressPrivate: Optional[bool] = None, + retryPolicyInitialInterval: Optional[int] = None, + retryPolicyMaxInterval: Optional[int] = None, + retryPolicyMaxAttempts: Optional[int] = None, + retryPolicyExponentiationFactor: Optional[float] = None, + retryPolicyOnMaxAttempts: Optional[str] = None, + ): self.name = name self.ty = ty self.input = input @@ -99,25 +111,28 @@ def __init__(self, self.retryPolicyExponentiationFactor = retryPolicyExponentiationFactor self.retryPolicyOnMaxAttempts = retryPolicyOnMaxAttempts + class Service: # pylint: disable=R0902,R0914 - def __init__(self, - name: str, - ty: ServiceType, - handlers: List[Handler], - description: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - inactivityTimeout: Optional[int] = None, - abortTimeout: Optional[int] = None, - journalRetention: Optional[int] = None, - idempotencyRetention: Optional[int] = None, - enableLazyState: Optional[bool] = None, - ingressPrivate: Optional[bool] = None, - retryPolicyInitialInterval: Optional[int] = None, - retryPolicyMaxInterval: Optional[int] = None, - retryPolicyMaxAttempts: Optional[int] = None, - retryPolicyExponentiationFactor: Optional[float] = None, - retryPolicyOnMaxAttempts: Optional[str] = None): + def __init__( + self, + name: str, + ty: ServiceType, + handlers: List[Handler], + description: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + inactivityTimeout: Optional[int] = None, + abortTimeout: Optional[int] = None, + journalRetention: Optional[int] = None, + idempotencyRetention: Optional[int] = None, + enableLazyState: Optional[bool] = None, + ingressPrivate: Optional[bool] = None, + retryPolicyInitialInterval: Optional[int] = None, + retryPolicyMaxInterval: Optional[int] = None, + retryPolicyMaxAttempts: Optional[int] = None, + retryPolicyExponentiationFactor: Optional[float] = None, + retryPolicyOnMaxAttempts: Optional[str] = None, + ): self.name = name self.ty = ty self.handlers = handlers @@ -135,31 +150,33 @@ def __init__(self, self.retryPolicyExponentiationFactor = retryPolicyExponentiationFactor self.retryPolicyOnMaxAttempts = retryPolicyOnMaxAttempts + class Endpoint: - def __init__(self, protocolMode: ProtocolMode, minProtocolVersion: int, maxProtocolVersion: int, services: List[Service]): + def __init__( + self, protocolMode: ProtocolMode, minProtocolVersion: int, maxProtocolVersion: int, services: List[Service] + ): self.protocolMode = protocolMode self.minProtocolVersion = minProtocolVersion self.maxProtocolVersion = maxProtocolVersion self.services = services -PROTOCOL_MODES = { - "bidi" : ProtocolMode.BIDI_STREAM, - "request_response" : ProtocolMode.REQUEST_RESPONSE} -SERVICE_TYPES = { - "service": ServiceType.SERVICE, - "object": ServiceType.VIRTUAL_OBJECT, - "workflow": ServiceType.WORKFLOW} +PROTOCOL_MODES = {"bidi": ProtocolMode.BIDI_STREAM, "request_response": ProtocolMode.REQUEST_RESPONSE} + +SERVICE_TYPES = {"service": ServiceType.SERVICE, "object": ServiceType.VIRTUAL_OBJECT, "workflow": ServiceType.WORKFLOW} + +HANDLER_TYPES = { + "exclusive": ServiceHandlerType.EXCLUSIVE, + "shared": ServiceHandlerType.SHARED, + "workflow": ServiceHandlerType.WORKFLOW, +} -HANDLER_TYPES = { - 'exclusive': ServiceHandlerType.EXCLUSIVE, - 'shared': ServiceHandlerType.SHARED, - 'workflow': ServiceHandlerType.WORKFLOW} class PythonClassEncoder(json.JSONEncoder): """ Serialize Python objects as JSON """ + def default(self, o): if isinstance(o, Enum): return o.value @@ -186,14 +203,13 @@ def type_hint_to_json_schema(type_hint: Any) -> Any: items = type_hint_to_json_schema(args[0] if args else Any) return {"type": "array", "items": items} if origin is dict: - return { - "type": "object" - } + return {"type": "object"} if origin is None: return {"type": "null"} # Default to all valid schema return True + def json_schema_from_type_hint(type_hint: Optional[TypeHint[Any]]) -> Any: """ Convert a type hint to a JSON schema. @@ -203,14 +219,14 @@ def json_schema_from_type_hint(type_hint: Optional[TypeHint[Any]]) -> Any: if not type_hint.annotation: return None if type_hint.is_pydantic: - return type_hint.annotation.model_json_schema(mode='serialization') + return type_hint.annotation.model_json_schema(mode="serialization") return type_hint_to_json_schema(type_hint.annotation) # pylint: disable=R0912,R0915 -def compute_discovery_json(endpoint: RestateEndpoint, - version: int, - discovered_as: typing.Literal["bidi", "request_response"]) -> str: +def compute_discovery_json( + endpoint: RestateEndpoint, version: int, discovered_as: typing.Literal["bidi", "request_response"] +) -> str: """ return restate's discovery object as JSON """ @@ -239,7 +255,9 @@ def compute_discovery_json(endpoint: RestateEndpoint, if handler.retryPolicyMaxAttempts is not None: raise ValueError("retryPolicyMaxAttempts is only supported in discovery protocol version 4") if handler.retryPolicyExponentiationFactor is not None: - raise ValueError("retryPolicyExponentiationFactor is only supported in discovery protocol version 4") + raise ValueError( + "retryPolicyExponentiationFactor is only supported in discovery protocol version 4" + ) if handler.retryPolicyOnMaxAttempts is not None: raise ValueError("retryPolicyOnMaxAttempts is only supported in discovery protocol version 4") @@ -279,7 +297,7 @@ def compute_discovery_json(endpoint: RestateEndpoint, return json_str -def compute_discovery(endpoint: RestateEndpoint, discovered_as : typing.Literal["bidi", "request_response"]) -> Endpoint: +def compute_discovery(endpoint: RestateEndpoint, discovered_as: typing.Literal["bidi", "request_response"]) -> Endpoint: """ return restate's discovery object for an endpoint """ @@ -299,60 +317,111 @@ def compute_discovery(endpoint: RestateEndpoint, discovered_as : typing.Literal[ if handler.handler_io.input_type and handler.handler_io.input_type.is_void: inp = {} else: - inp = InputPayload(required=False, - contentType=handler.handler_io.accept, - jsonSchema=json_schema_from_type_hint(handler.handler_io.input_type)) + inp = InputPayload( + required=False, + contentType=handler.handler_io.accept, + jsonSchema=json_schema_from_type_hint(handler.handler_io.input_type), + ) # output if handler.handler_io.output_type and handler.handler_io.output_type.is_void: out = OutputPayload(setContentTypeIfEmpty=False) else: - out = OutputPayload(setContentTypeIfEmpty=False, - contentType=handler.handler_io.content_type, - jsonSchema=json_schema_from_type_hint(handler.handler_io.output_type)) + out = OutputPayload( + setContentTypeIfEmpty=False, + contentType=handler.handler_io.content_type, + jsonSchema=json_schema_from_type_hint(handler.handler_io.output_type), + ) # add the handler - service_handlers.append(Handler(name=handler.name, - ty=ty, - input=inp, - output=out, - description=handler.description, - metadata=handler.metadata, - inactivityTimeout=int(handler.inactivity_timeout.total_seconds() * 1000) if handler.inactivity_timeout else None, - abortTimeout=int(handler.abort_timeout.total_seconds() * 1000) if handler.abort_timeout else None, - journalRetention=int(handler.journal_retention.total_seconds() * 1000) if handler.journal_retention else None, - idempotencyRetention=int(handler.idempotency_retention.total_seconds() * 1000) if handler.idempotency_retention else None, - workflowCompletionRetention=int(handler.workflow_retention.total_seconds() * 1000) if handler.workflow_retention else None, - enableLazyState=handler.enable_lazy_state, - ingressPrivate=handler.ingress_private, - retryPolicyInitialInterval=int(handler.invocation_retry_policy.initial_interval.total_seconds() * 1000) if handler.invocation_retry_policy and handler.invocation_retry_policy.initial_interval else None, - retryPolicyMaxInterval=int(handler.invocation_retry_policy.max_interval.total_seconds() * 1000) if handler.invocation_retry_policy and handler.invocation_retry_policy.max_interval else None, - retryPolicyMaxAttempts=int(handler.invocation_retry_policy.max_attempts) if handler.invocation_retry_policy and handler.invocation_retry_policy.max_attempts is not None else None, - retryPolicyExponentiationFactor=float(handler.invocation_retry_policy.exponentiation_factor) if handler.invocation_retry_policy and handler.invocation_retry_policy.exponentiation_factor is not None else None, - retryPolicyOnMaxAttempts=(handler.invocation_retry_policy.on_max_attempts.upper() if handler.invocation_retry_policy and handler.invocation_retry_policy.on_max_attempts is not None else None))) + service_handlers.append( + Handler( + name=handler.name, + ty=ty, + input=inp, + output=out, + description=handler.description, + metadata=handler.metadata, + inactivityTimeout=int(handler.inactivity_timeout.total_seconds() * 1000) + if handler.inactivity_timeout + else None, + abortTimeout=int(handler.abort_timeout.total_seconds() * 1000) if handler.abort_timeout else None, + journalRetention=int(handler.journal_retention.total_seconds() * 1000) + if handler.journal_retention + else None, + idempotencyRetention=int(handler.idempotency_retention.total_seconds() * 1000) + if handler.idempotency_retention + else None, + workflowCompletionRetention=int(handler.workflow_retention.total_seconds() * 1000) + if handler.workflow_retention + else None, + enableLazyState=handler.enable_lazy_state, + ingressPrivate=handler.ingress_private, + retryPolicyInitialInterval=int( + handler.invocation_retry_policy.initial_interval.total_seconds() * 1000 + ) + if handler.invocation_retry_policy and handler.invocation_retry_policy.initial_interval + else None, + retryPolicyMaxInterval=int(handler.invocation_retry_policy.max_interval.total_seconds() * 1000) + if handler.invocation_retry_policy and handler.invocation_retry_policy.max_interval + else None, + retryPolicyMaxAttempts=int(handler.invocation_retry_policy.max_attempts) + if handler.invocation_retry_policy and handler.invocation_retry_policy.max_attempts is not None + else None, + retryPolicyExponentiationFactor=float(handler.invocation_retry_policy.exponentiation_factor) + if handler.invocation_retry_policy + and handler.invocation_retry_policy.exponentiation_factor is not None + else None, + retryPolicyOnMaxAttempts=( + handler.invocation_retry_policy.on_max_attempts.upper() + if handler.invocation_retry_policy + and handler.invocation_retry_policy.on_max_attempts is not None + else None + ), + ) + ) # add the service description = service.service_tag.description metadata = service.service_tag.metadata - services.append(Service(name=service.name, - ty=service_type, - handlers=service_handlers, - description=description, - metadata=metadata, - inactivityTimeout=int(service.inactivity_timeout.total_seconds() * 1000) if service.inactivity_timeout else None, - abortTimeout=int(service.abort_timeout.total_seconds() * 1000) if service.abort_timeout else None, - journalRetention=int(service.journal_retention.total_seconds() * 1000) if service.journal_retention else None, - idempotencyRetention=int(service.idempotency_retention.total_seconds() * 1000) if service.idempotency_retention else None, - enableLazyState=service.enable_lazy_state if hasattr(service, 'enable_lazy_state') else None, - ingressPrivate=service.ingress_private, - retryPolicyInitialInterval=int(service.invocation_retry_policy.initial_interval.total_seconds() * 1000) if service.invocation_retry_policy and service.invocation_retry_policy.initial_interval else None, - retryPolicyMaxInterval=int(service.invocation_retry_policy.max_interval.total_seconds() * 1000) if service.invocation_retry_policy and service.invocation_retry_policy.max_interval else None, - retryPolicyMaxAttempts=int(service.invocation_retry_policy.max_attempts) if service.invocation_retry_policy and service.invocation_retry_policy.max_attempts is not None else None, - retryPolicyExponentiationFactor=float(service.invocation_retry_policy.exponentiation_factor) if service.invocation_retry_policy and service.invocation_retry_policy.exponentiation_factor is not None else None, - retryPolicyOnMaxAttempts=(service.invocation_retry_policy.on_max_attempts.upper() if service.invocation_retry_policy and service.invocation_retry_policy.on_max_attempts is not None else None))) + services.append( + Service( + name=service.name, + ty=service_type, + handlers=service_handlers, + description=description, + metadata=metadata, + inactivityTimeout=int(service.inactivity_timeout.total_seconds() * 1000) + if service.inactivity_timeout + else None, + abortTimeout=int(service.abort_timeout.total_seconds() * 1000) if service.abort_timeout else None, + journalRetention=int(service.journal_retention.total_seconds() * 1000) + if service.journal_retention + else None, + idempotencyRetention=int(service.idempotency_retention.total_seconds() * 1000) + if service.idempotency_retention + else None, + enableLazyState=service.enable_lazy_state if isinstance(service, (Workflow, VirtualObject)) else None, + ingressPrivate=service.ingress_private, + retryPolicyInitialInterval=int(service.invocation_retry_policy.initial_interval.total_seconds() * 1000) + if service.invocation_retry_policy and service.invocation_retry_policy.initial_interval + else None, + retryPolicyMaxInterval=int(service.invocation_retry_policy.max_interval.total_seconds() * 1000) + if service.invocation_retry_policy and service.invocation_retry_policy.max_interval + else None, + retryPolicyMaxAttempts=int(service.invocation_retry_policy.max_attempts) + if service.invocation_retry_policy and service.invocation_retry_policy.max_attempts is not None + else None, + retryPolicyExponentiationFactor=float(service.invocation_retry_policy.exponentiation_factor) + if service.invocation_retry_policy and service.invocation_retry_policy.exponentiation_factor is not None + else None, + retryPolicyOnMaxAttempts=( + service.invocation_retry_policy.on_max_attempts.upper() + if service.invocation_retry_policy and service.invocation_retry_policy.on_max_attempts is not None + else None + ), + ) + ) if endpoint.protocol: protocol_mode = PROTOCOL_MODES[endpoint.protocol] else: protocol_mode = PROTOCOL_MODES[discovered_as] - return Endpoint(protocolMode=protocol_mode, - minProtocolVersion=5, - maxProtocolVersion=5, - services=services) + return Endpoint(protocolMode=protocol_mode, minProtocolVersion=5, maxProtocolVersion=5, services=services) diff --git a/python/restate/endpoint.py b/python/restate/endpoint.py index cee0270..2383101 100644 --- a/python/restate/endpoint.py +++ b/python/restate/endpoint.py @@ -1,4 +1,3 @@ - # # Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH # @@ -89,17 +88,20 @@ def app(self): Returns: The ASGI application for this endpoint. - """ + """ # we need to import it here to avoid circular dependencies # pylint: disable=C0415 # pylint: disable=R0401 from restate.server import asgi_app + return asgi_app(self) + def app( services: typing.Iterable[typing.Union[Service, VirtualObject, Workflow]], protocol: typing.Optional[typing.Literal["bidi", "request_response"]] = None, - identity_keys: typing.Optional[typing.List[str]] = None): + identity_keys: typing.Optional[typing.List[str]] = None, +): """A restate ASGI application that hosts the given services.""" endpoint = Endpoint() if protocol == "bidi": diff --git a/python/restate/exceptions.py b/python/restate/exceptions.py index cbd6a6a..22b8498 100644 --- a/python/restate/exceptions.py +++ b/python/restate/exceptions.py @@ -12,21 +12,24 @@ # pylint: disable=C0301 + class TerminalError(Exception): """This exception is thrown to indicate that Restate should not retry.""" + def __init__(self, message: str, status_code: int = 500) -> None: super().__init__(message) self.message = message self.status_code = status_code -class SdkInternalBaseException(Exception): +class SdkInternalBaseException(BaseException): """This exception is internal, and you should not catch it. If you need to distinguish with other exceptions, use is_internal_exception.""" + def __init__(self, message: str) -> None: super().__init__( - message + -""" + message + + """ This exception is safe to ignore. If you see it, you might be using a try/catch all statement. Don't do: @@ -43,18 +46,25 @@ def __init__(self, message: str) -> None: Or remove the try/except altogether if you don't need it. For further info on error handling, refer to https://docs.restate.dev/develop/python/error-handling -""") +""" + ) + class SuspendedException(SdkInternalBaseException): """This exception is raised to indicate that the execution is suspended""" + def __init__(self) -> None: super().__init__("Invocation got suspended, Restate will resume this invocation when progress can be made.") + class SdkInternalException(SdkInternalBaseException): """This exception is raised to indicate that the execution raised a retryable error.""" + def __init__(self) -> None: - super().__init__("Invocation attempt raised a retryable error.\n" - "Restate will retry executing this invocation from the point where it left off.") + super().__init__( + "Invocation attempt raised a retryable error.\n" + "Restate will retry executing this invocation from the point where it left off." + ) def is_internal_exception(e) -> bool: diff --git a/python/restate/handler.py b/python/restate/handler.py index 481c160..da2a74f 100644 --- a/python/restate/handler.py +++ b/python/restate/handler.py @@ -26,32 +26,37 @@ from restate.exceptions import TerminalError from restate.serde import DefaultSerde, PydanticJsonSerde, Serde, is_pydantic -I = TypeVar('I') -O = TypeVar('O') -T = TypeVar('T') +I = TypeVar("I") +O = TypeVar("O") +T = TypeVar("T") # we will use this symbol to store the handler in the function RESTATE_UNIQUE_HANDLER_SYMBOL = str(object()) + @dataclass class ServiceTag: """ This class is used to identify the service. """ + kind: Literal["object", "service", "workflow"] name: str description: Optional[str] = None metadata: Optional[Dict[str, str]] = None + @dataclass class TypeHint(Generic[T]): """ Represents a type hint. """ + annotation: Optional[T] = None is_pydantic: bool = False is_void: bool = False + @dataclass class HandlerIO(Generic[I, O]): """ @@ -61,6 +66,7 @@ class HandlerIO(Generic[I, O]): accept (str): The accept header value for the handler. content_type (str): The content type header value for the handler. """ + accept: str content_type: str input_serde: Serde[I] @@ -98,10 +104,11 @@ def update_handler_io_with_type_hints(handler_io: HandlerIO[I, O], signature: Si else: handler_io.output_type = TypeHint(annotation=annotation, is_pydantic=False) if is_pydantic(annotation): - handler_io.output_type.is_pydantic=True + handler_io.output_type.is_pydantic = True if isinstance(handler_io.output_serde, DefaultSerde): handler_io.output_serde = PydanticJsonSerde(annotation) + # pylint: disable=R0902 @dataclass class Handler(Generic[I, O]): @@ -126,6 +133,7 @@ class Handler(Generic[I, O]): ingress_private: If true, the handler cannot be invoked from the HTTP nor Kafka ingress. invocation_retry_policy: Optional retry policy configuration applied to this handler. """ + service_tag: ServiceTag handler_io: HandlerIO[I, O] kind: Optional[Literal["exclusive", "shared", "workflow"]] @@ -147,22 +155,24 @@ class Handler(Generic[I, O]): # disable too many arguments warning # pylint: disable=R0913 # pylint: disable=R0914 -def make_handler(service_tag: ServiceTag, - handler_io: HandlerIO[I, O], - name: str | None, - kind: Optional[Literal["exclusive", "shared", "workflow"]], - wrapped: Any, - signature: Signature, - description: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - inactivity_timeout: Optional[timedelta] = None, - abort_timeout: Optional[timedelta] = None, - journal_retention: Optional[timedelta] = None, - idempotency_retention: Optional[timedelta] = None, - workflow_retention: Optional[timedelta] = None, - enable_lazy_state: Optional[bool] = None, - ingress_private: Optional[bool] = None, - invocation_retry_policy: Optional[InvocationRetryPolicy] = None) -> Handler[I, O]: +def make_handler( + service_tag: ServiceTag, + handler_io: HandlerIO[I, O], + name: str | None, + kind: Optional[Literal["exclusive", "shared", "workflow"]], + wrapped: Any, + signature: Signature, + description: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + inactivity_timeout: Optional[timedelta] = None, + abort_timeout: Optional[timedelta] = None, + journal_retention: Optional[timedelta] = None, + idempotency_retention: Optional[timedelta] = None, + workflow_retention: Optional[timedelta] = None, + enable_lazy_state: Optional[bool] = None, + ingress_private: Optional[bool] = None, + invocation_retry_policy: Optional[InvocationRetryPolicy] = None, +) -> Handler[I, O]: """ Factory function to create a handler. @@ -196,28 +206,31 @@ def make_handler(service_tag: ServiceTag, raise ValueError("Handler must have at least one parameter") arity = len(signature.parameters) - update_handler_io_with_type_hints(handler_io, signature) # mutates handler_io - - handler = Handler[I, O](service_tag=service_tag, - handler_io=handler_io, - kind=kind, - name=handler_name, - fn=wrapped, - arity=arity, - description=description, - metadata=metadata, - inactivity_timeout=inactivity_timeout, - abort_timeout=abort_timeout, - journal_retention=journal_retention, - idempotency_retention=idempotency_retention, - workflow_retention=workflow_retention, - enable_lazy_state=enable_lazy_state, - ingress_private=ingress_private, - invocation_retry_policy=invocation_retry_policy) + update_handler_io_with_type_hints(handler_io, signature) # mutates handler_io + + handler = Handler[I, O]( + service_tag=service_tag, + handler_io=handler_io, + kind=kind, + name=handler_name, + fn=wrapped, + arity=arity, + description=description, + metadata=metadata, + inactivity_timeout=inactivity_timeout, + abort_timeout=abort_timeout, + journal_retention=journal_retention, + idempotency_retention=idempotency_retention, + workflow_retention=workflow_retention, + enable_lazy_state=enable_lazy_state, + ingress_private=ingress_private, + invocation_retry_policy=invocation_retry_policy, + ) vars(wrapped)[RESTATE_UNIQUE_HANDLER_SYMBOL] = handler return handler + def handler_from_callable(wrapper: HandlerType[I, O]) -> Handler[I, O]: """ Get the handler from the callable. @@ -225,7 +238,8 @@ def handler_from_callable(wrapper: HandlerType[I, O]) -> Handler[I, O]: try: return vars(wrapper)[RESTATE_UNIQUE_HANDLER_SYMBOL] except KeyError: - raise ValueError("Handler not found") # pylint: disable=raise-missing-from + raise ValueError("Handler not found") # pylint: disable=raise-missing-from + async def invoke_handler(handler: Handler[I, O], ctx: Any, in_buffer: bytes) -> bytes: """ @@ -236,8 +250,8 @@ async def invoke_handler(handler: Handler[I, O], ctx: Any, in_buffer: bytes) -> in_arg = handler.handler_io.input_serde.deserialize(in_buffer) except Exception as e: raise TerminalError(message=f"Unable to parse an input argument. {e}") from e - out_arg = await handler.fn(ctx, in_arg) # type: ignore [call-arg, arg-type] + out_arg = await handler.fn(ctx, in_arg) # type: ignore [call-arg, arg-type] else: - out_arg = await handler.fn(ctx) # type: ignore [call-arg] + out_arg = await handler.fn(ctx) # type: ignore [call-arg] out_buffer = handler.handler_io.output_serde.serialize(out_arg) return bytes(out_buffer) diff --git a/python/restate/harness.py b/python/restate/harness.py index a5663fa..3ff1e59 100644 --- a/python/restate/harness.py +++ b/python/restate/harness.py @@ -17,11 +17,15 @@ import typing from urllib.error import URLError import socket +from contextlib import contextmanager +from warnings import deprecated from hypercorn.config import Config from hypercorn.asyncio import serve -from testcontainers.core.container import DockerContainer # type: ignore -from testcontainers.core.waiting_utils import wait_container_is_ready # type: ignore +from restate.server_types import RestateAppT +from restate.types import TestHarnessEnvironment +from testcontainers.core.container import DockerContainer # type: ignore +from testcontainers.core.waiting_utils import wait_container_is_ready # type: ignore import httpx @@ -31,8 +35,10 @@ def find_free_port(): s.bind(("0.0.0.0", 0)) return s.getsockname()[1] + def run_in_background(coro) -> threading.Thread: """run a coroutine in the background""" + def runner(): asyncio.run(coro) @@ -56,6 +62,7 @@ def get_endpoint_connection_string(self) -> str: def cleanup(self): """cleanup any resources used by the bind address""" + class TcpSocketBindAddress(BindAddress): """Bind a TCP address that listens on a random TCP port""" @@ -109,13 +116,10 @@ async def run_asgi(): config.bind = [bind] try: print(f"Starting ASGI server on {bind}", flush=True) - await serve(self.asgi_app, - config=config, - mode='asgi', - shutdown_trigger=shutdown_trigger) + await serve(self.asgi_app, config=config, mode="asgi", shutdown_trigger=shutdown_trigger) except asyncio.CancelledError: print("ASGI server was cancelled", flush=True) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except print(f"Failed to start the ASGI server: {e}", flush=True) raise e finally: @@ -124,6 +128,7 @@ async def run_asgi(): self.thread = run_in_background(run_asgi()) return self + class RestateContainer(DockerContainer): """Create a Restate container""" @@ -132,21 +137,21 @@ class RestateContainer(DockerContainer): def __init__(self, image, always_replay, disable_retries): super().__init__(image) self.with_exposed_ports(8080, 9070) - self.with_env('RESTATE_LOG_FILTER', 'restate=info') - self.with_env('RESTATE_BOOTSTRAP_NUM_PARTITIONS', '1') - self.with_env('RESTATE_DEFAULT_NUM_PARTITIONS', '1') - self.with_env('RESTATE_SHUTDOWN_TIMEOUT', '10s') - self.with_env('RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE', '32 MB') - self.with_env('RESTATE_WORKER__INVOKER__IN_MEMORY_QUEUE_LENGTH_LIMIT', '64') + self.with_env("RESTATE_LOG_FILTER", "restate=info") + self.with_env("RESTATE_BOOTSTRAP_NUM_PARTITIONS", "1") + self.with_env("RESTATE_DEFAULT_NUM_PARTITIONS", "1") + self.with_env("RESTATE_SHUTDOWN_TIMEOUT", "10s") + self.with_env("RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE", "32 MB") + self.with_env("RESTATE_WORKER__INVOKER__IN_MEMORY_QUEUE_LENGTH_LIMIT", "64") if always_replay: - self.with_env('RESTATE_WORKER__INVOKER__INACTIVITY_TIMEOUT', '0s') + self.with_env("RESTATE_WORKER__INVOKER__INACTIVITY_TIMEOUT", "0s") else: - self.with_env('RESTATE_WORKER__INVOKER__INACTIVITY_TIMEOUT', '10m') - self.with_env('RESTATE_WORKER__INVOKER__ABORT_TIMEOUT', '10m') + self.with_env("RESTATE_WORKER__INVOKER__INACTIVITY_TIMEOUT", "10m") + self.with_env("RESTATE_WORKER__INVOKER__ABORT_TIMEOUT", "10m") if disable_retries: - self.with_env('RESTATE_WORKER__INVOKER__RETRY_POLICY__TYPE', 'none') + self.with_env("RESTATE_WORKER__INVOKER__RETRY_POLICY__TYPE", "none") - self.with_kwargs(extra_hosts={"host.docker.internal" : "host-gateway"}) + self.with_kwargs(extra_hosts={"host.docker.internal": "host-gateway"}) def ingress_url(self): """return the URL to access the Restate ingress""" @@ -170,8 +175,7 @@ def _wait_healthy(self): self.get_ingress_client().get("/restate/health").raise_for_status() self.get_admin_client().get("/health").raise_for_status() - - def start(self, stream_logs = False): + def start(self, stream_logs=False): """start the container and wait for health checks to pass""" super().start() @@ -191,6 +195,7 @@ def stream_log(): @dataclass class TestConfiguration: """A configuration for running tests""" + restate_image: str = "restatedev/restate:latest" stream_logs: bool = False always_replay: bool = False @@ -199,6 +204,7 @@ class TestConfiguration: class RestateTestHarness: """A test harness for running Restate SDKs""" + bind_address: typing.Optional[BindAddress] = None server: typing.Optional[AsgiServer] = None restate: typing.Optional[RestateContainer] = None @@ -217,8 +223,8 @@ def start(self): self.restate = RestateContainer( image=self.config.restate_image, always_replay=self.config.always_replay, - disable_retries=self.config.disable_retries) \ - .start(self.config.stream_logs) + disable_retries=self.config.disable_retries, + ).start(self.config.stream_logs) try: self._register_sdk() except Exception as e: @@ -232,9 +238,7 @@ def _register_sdk(self): uri = self.bind_address.get_endpoint_connection_string() client = self.restate.get_admin_client() - res = client.post("/deployments", - headers={"content-type" : "application/json"}, - json={"uri": uri}) + res = client.post("/deployments", headers={"content-type": "application/json"}, json={"uri": uri}) if not res.is_success: msg = f"unable to register the services at {uri} - {res.status_code} {res.text}" raise AssertionError(msg) @@ -254,7 +258,6 @@ def ingress_client(self): raise AssertionError("The Restate server has not been started. Use .start()") return self.restate.get_ingress_client() - def __enter__(self): self.start() return self @@ -264,29 +267,115 @@ def __exit__(self, exc_type, exc_value, traceback): return False -def test_harness(app, - follow_logs: bool = False, - restate_image: str = "restatedev/restate:latest", - always_replay: bool = False, - disable_retries: bool = False) -> RestateTestHarness: +@contextmanager +def create_app_server( + app: RestateAppT, +) -> typing.Generator[str, None, None]: """ - Creates a test harness for running Restate services together with restate-server. + Creates and starts an ASGI server for the given application. + + :param app: The ASGI application to be served. + """ + bind_address = TcpSocketBindAddress() + server = AsgiServer(app, bind_address).start() + try: + yield bind_address.get_endpoint_connection_string() + finally: + server.stop() + + +@contextmanager +def create_restate_container( + restate_image: str = "restatedev/restate:latest", + always_replay: bool = False, + disable_retries: bool = False, + stream_logs: bool = False, +) -> typing.Generator[RestateContainer, None, None]: + """ + Creates and starts a Restate container. - :param app: The application to be tested using the RestateTestHarness. - :param follow_logs: Whether to stream logs for the test process (default is False). :param restate_image: The image name for the restate-server container (default is "restatedev/restate:latest"). :param always_replay: When True, this forces restate-server to always replay on a suspension point. This is useful to hunt non deterministic bugs that might prevent your code to replay correctly (default is False). :param disable_retries: When True, retries are disabled (default is False). - :return: An instance of RestateTestHarness initialized with the provided app and configuration. - :rtype: RestateTestHarness + """ + restate = RestateContainer( + image=restate_image, + always_replay=always_replay, + disable_retries=disable_retries, + ).start(stream_logs) + try: + yield restate + finally: + restate.stop() + + +@deprecated("Use create_test_harness instead") +def test_harness( + app: RestateAppT, + follow_logs: bool = False, + restate_image: str = "restatedev/restate:latest", + always_replay: bool = False, + disable_retries: bool = False, +) -> RestateTestHarness: + """ + Creates a test harness for running Restate services together with restate-server. """ config = TestConfiguration( restate_image=restate_image, stream_logs=follow_logs, always_replay=always_replay, - disable_retries=disable_retries + disable_retries=disable_retries, ) return RestateTestHarness(app, config) + + +@contextmanager +def create_test_harness( + app: RestateAppT, + follow_logs: bool = False, + restate_image: str = "restatedev/restate:latest", + always_replay: bool = False, + disable_retries: bool = False, +) -> typing.Generator[TestHarnessEnvironment, None, None]: + """ + Creates a test harness for running Restate services together with restate-server. + + example: + ``` + from restate import create_test_harness + from my_app import my_restate_app + + with create_test_harness(my_restate_app) as env: + client = env.create_ingress_client() + # run tests against the client + ``` + + :param app: The application to be tested using the RestateTestHarness. + :param follow_logs: Whether to stream logs for the test process (default is False). + :param restate_image: The image name for the restate-server container + (default is "restatedev/restate:latest"). + :param always_replay: When True, this forces restate-server to always replay + on a suspension point. This is useful to hunt non deterministic bugs + that might prevent your code to replay correctly (default is False). + :param disable_retries: When True, retries are disabled (default is False). + """ + with ( + create_restate_container( + restate_image=restate_image, + always_replay=always_replay, + disable_retries=disable_retries, + stream_logs=follow_logs, + ) as runtime, + create_app_server(app) as bind_address, + ): + res = runtime.get_admin_client().post( + "/deployments", headers={"content-type": "application/json"}, json={"uri": bind_address} + ) + if not res.is_success: + msg = f"unable to register the services at {bind_address} - {res.status_code} {res.text}" + raise AssertionError(msg) + + yield TestHarnessEnvironment(ingress_url=runtime.ingress_url(), admin_api_url=runtime.admin_url()) diff --git a/python/restate/logging.py b/python/restate/logging.py index 6038594..31a39f5 100644 --- a/python/restate/logging.py +++ b/python/restate/logging.py @@ -11,10 +11,12 @@ """ This module contains the logging utilities for restate handlers. """ + import logging from .server_context import restate_context_is_replaying + # pylint: disable=C0103 def getLogger(name=None): """ @@ -27,6 +29,7 @@ def getLogger(name=None): logger.addFilter(RestateLoggingFilter()) return logger + # pylint: disable=R0903 class RestateLoggingFilter(logging.Filter): """ diff --git a/python/restate/object.py b/python/restate/object.py index 0a926a5..4030afc 100644 --- a/python/restate/object.py +++ b/python/restate/object.py @@ -23,9 +23,9 @@ from restate.retry_policy import InvocationRetryPolicy from restate.handler import Handler, HandlerIO, ServiceTag, make_handler -I = typing.TypeVar('I') -O = typing.TypeVar('O') - +I = typing.TypeVar("I") +O = typing.TypeVar("O") +T = typing.TypeVar("T") # disable too many arguments warning # pylint: disable=R0913 @@ -33,6 +33,7 @@ # disable line too long warning # pylint: disable=C0301 + # pylint: disable=R0902 class VirtualObject: """ @@ -74,16 +75,19 @@ class VirtualObject: handlers: typing.Dict[str, Handler[typing.Any, typing.Any]] - def __init__(self, name, - description: typing.Optional[str] = None, - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - enable_lazy_state: typing.Optional[bool] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None): + def __init__( + self, + name, + description: typing.Optional[str] = None, + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + enable_lazy_state: typing.Optional[bool] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ): self.service_tag = ServiceTag("object", name, description, metadata) self.handlers = {} self.inactivity_timeout = inactivity_timeout @@ -102,26 +106,28 @@ def name(self): return self.service_tag.name # pylint: disable=R0914 - def handler(self, - name: typing.Optional[str] = None, - kind: typing.Optional[typing.Literal["exclusive", "shared"]] = "exclusive", - accept: str = "application/json", - content_type: str = "application/json", - input_serde: Serde[I] = DefaultSerde(), - output_serde: Serde[O] = DefaultSerde(), - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - enable_lazy_state: typing.Optional[bool] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None) -> typing.Callable: + def handler( + self, + name: typing.Optional[str] = None, + kind: typing.Optional[typing.Literal["exclusive", "shared"]] = "exclusive", + accept: str = "application/json", + content_type: str = "application/json", + input_serde: Serde[I] = DefaultSerde(), + output_serde: Serde[O] = DefaultSerde(), + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + enable_lazy_state: typing.Optional[bool] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ) -> typing.Callable[[T], T]: """ Decorator for defining a handler function. Args: - name: The name of the handler. + name: The name of the handler. kind: The kind of handler (exclusive, shared). Default "exclusive". accept: Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g. `application/*` or `*/*`. Default "application/json". @@ -170,18 +176,33 @@ def my_handler_func(ctx, request): # handler logic pass """ - handler_io = HandlerIO[I,O](accept, content_type, input_serde, output_serde) - def wrapper(fn): + handler_io = HandlerIO[I, O](accept, content_type, input_serde, output_serde) + def wrapper(fn): @wraps(fn) def wrapped(*args, **kwargs): return fn(*args, **kwargs) signature = inspect.signature(fn, eval_str=True) - handler = make_handler(self.service_tag, handler_io, name, kind, wrapped, signature, inspect.getdoc(fn), metadata, - inactivity_timeout, abort_timeout, journal_retention, idempotency_retention, - None, enable_lazy_state, ingress_private, invocation_retry_policy) + handler = make_handler( + self.service_tag, + handler_io, + name, + kind, + wrapped, + signature, + inspect.getdoc(fn), + metadata, + inactivity_timeout, + abort_timeout, + journal_retention, + idempotency_retention, + None, + enable_lazy_state, + ingress_private, + invocation_retry_policy, + ) self.handlers[handler.name] = handler return wrapped - return wrapper + return typing.cast(typing.Callable[[T], T], wrapper) diff --git a/python/restate/retry_policy.py b/python/restate/retry_policy.py index 09e3022..8f34e1b 100644 --- a/python/restate/retry_policy.py +++ b/python/restate/retry_policy.py @@ -12,12 +12,14 @@ Note: You can set these fields only if you register this service against restate-server >= 1.5 and discovery protocol v4. Otherwise, service discovery will fail. """ + from __future__ import annotations from dataclasses import dataclass from datetime import timedelta from typing import Optional, Literal + @dataclass class InvocationRetryPolicy: """ diff --git a/python/restate/serde.py b/python/restate/serde.py index 5165a17..c415981 100644 --- a/python/restate/serde.py +++ b/python/restate/serde.py @@ -8,68 +8,84 @@ # directory of this repository or package, or at # https://github.com/restatedev/sdk-typescript/blob/main/LICENSE # -""" This module contains functions for serializing and deserializing data. """ +"""This module contains functions for serializing and deserializing data.""" + import abc import json import typing from dataclasses import asdict, is_dataclass + def try_import_pydantic_base_model(): """ Try to import PydanticBaseModel from Pydantic. """ try: - from pydantic import BaseModel # type: ignore # pylint: disable=import-outside-toplevel + from pydantic import BaseModel # type: ignore # pylint: disable=import-outside-toplevel + return BaseModel except ImportError: - class Dummy: # pylint: disable=too-few-public-methods + + class Dummy: # pylint: disable=too-few-public-methods """a dummy class to use when Pydantic is not available""" return Dummy + def try_import_from_dacite(): """ Try to import from_dict from dacite. """ try: - from dacite import from_dict # type: ignore # pylint: disable=import-outside-toplevel + from dacite import from_dict # type: ignore # pylint: disable=import-outside-toplevel - return asdict, from_dict + def _to_dict(obj: typing.Any) -> dict[typing.Any, typing.Any]: + return asdict(obj) - except ImportError: + def _from_dict(data_class: typing.Any, data: typing.Any) -> typing.Any: + return from_dict(data_class, data) - def to_dict(obj): - """a dummy function when dacite is not available""" - raise RuntimeError("Trying to deserialize into a @dataclass." \ - "Please add the optional dependencies needed." \ - "use pip install restate-sdk[serde] " - "or" \ - " pip install restate-sdk[all] to install all dependencies.") + return _to_dict, _from_dict + except ImportError: - def from_dict(a,b): # pylint: disable=too-few-public-methods,unused-argument + def _to_dict(obj: typing.Any) -> dict[typing.Any, typing.Any]: + """a dummy function when dacite is not available""" + raise RuntimeError( + "Trying to deserialize into a @dataclass." + "Please add the optional dependencies needed." + "use pip install restate-sdk[serde] " + "or" + " pip install restate-sdk[all] to install all dependencies." + ) + + def _from_dict(data_class: typing.Any, data: typing.Any) -> typing.Any: # pylint: disable=too-few-public-methods,unused-argument """a dummy function when dacite is not available""" - raise RuntimeError("Trying to deserialize into a @dataclass." \ - "Please add the optional dependencies needed." \ - "use pip install restate-sdk[serde] " - "or" \ - " pip install restate-sdk[all] to install all dependencies.") + raise RuntimeError( + "Trying to deserialize into a @dataclass." + "Please add the optional dependencies needed." + "use pip install restate-sdk[serde] " + "or" + " pip install restate-sdk[all] to install all dependencies." + ) + + return _to_dict, _from_dict - return to_dict, from_dict PydanticBaseModel = try_import_pydantic_base_model() # pylint: disable=C0103 DaciteToDict, DaciteFromDict = try_import_from_dacite() -T = typing.TypeVar('T') -I = typing.TypeVar('I') -O = typing.TypeVar('O') +T = typing.TypeVar("T") +I = typing.TypeVar("I") +O = typing.TypeVar("O") # disable to few parameters # pylint: disable=R0903 + def is_pydantic(annotation) -> bool: """ Check if an object is a Pydantic model. @@ -80,6 +96,7 @@ def is_pydantic(annotation) -> bool: # annotation is not a class or a type return False + class Serde(typing.Generic[T], abc.ABC): """serializer/deserializer interface.""" @@ -95,6 +112,7 @@ def serialize(self, obj: typing.Optional[T]) -> bytes: Serializes an object to a bytearray. """ + class BytesSerde(Serde[bytes]): """A pass-trough serializer/deserializer.""" @@ -157,6 +175,7 @@ def serialize(self, obj: typing.Optional[I]) -> bytes: return bytes(json.dumps(obj), "utf-8") + class DefaultSerde(Serde[I]): """ The default serializer/deserializer used when no explicit type hints are provided. @@ -168,15 +187,15 @@ class DefaultSerde(Serde[I]): - Otherwise, it falls back to `json.dumps()`. - Deserialization: - Uses `json.loads()` to convert byte arrays into Python objects. - - Does **not** automatically reconstruct Pydantic models; + - Does **not** automatically reconstruct Pydantic models; deserialized objects remain as generic JSON structures (dicts, lists, etc.). Serde Selection: - - When using the `@handler` decorator, if a function's type hints specify a Pydantic model, + - When using the `@handler` decorator, if a function's type hints specify a Pydantic model, `PydanticJsonSerde` is automatically selected instead of `DefaultSerde`. - `DefaultSerde` is only used if no explicit type hints are provided. - This serde ensures compatibility with both structured (Pydantic) and unstructured JSON data, + This serde ensures compatibility with both structured (Pydantic) and unstructured JSON data, while allowing automatic serde selection based on type hints. """ @@ -209,7 +228,7 @@ def deserialize(self, buf: bytes) -> typing.Optional[I]: if not buf: return None if is_pydantic(self.type_hint): - return self.type_hint.model_validate_json(buf) # type: ignore + return self.type_hint.model_validate_json(buf) # type: ignore if is_dataclass(self.type_hint): data = json.loads(buf) return DaciteFromDict(self.type_hint, data) @@ -231,7 +250,7 @@ def serialize(self, obj: typing.Optional[I]) -> bytes: if is_pydantic(self.type_hint): return obj.model_dump_json().encode("utf-8") # type: ignore[attr-defined] if is_dataclass(obj): - data = DaciteToDict(obj) # type: ignore + data = DaciteToDict(obj) # type: ignore return json.dumps(data).encode("utf-8") return json.dumps(obj).encode("utf-8") @@ -270,5 +289,5 @@ def serialize(self, obj: typing.Optional[I]) -> bytes: """ if obj is None: return bytes() - json_str = obj.model_dump_json() # type: ignore[attr-defined] + json_str = obj.model_dump_json() # type: ignore[attr-defined] return json_str.encode("utf-8") diff --git a/python/restate/server.py b/python/restate/server.py index cebaebb..fc6dd8d 100644 --- a/python/restate/server.py +++ b/python/restate/server.py @@ -13,48 +13,53 @@ import asyncio from typing import Dict, TypedDict, Literal import traceback + from restate.discovery import compute_discovery_json from restate.endpoint import Endpoint from restate.server_context import ServerInvocationContext, DisconnectedException -from restate.server_types import Receive, ReceiveChannel, Scope, Send, binary_to_header, header_to_binary # pylint: disable=line-too-long +from restate.server_types import Receive, ReceiveChannel, RestateAppT, Scope, Send, binary_to_header, header_to_binary # pylint: disable=line-too-long from restate.vm import VMWrapper -from restate._internal import PyIdentityVerifier, IdentityVerificationException # pylint: disable=import-error,no-name-in-module -from restate._internal import SDK_VERSION # pylint: disable=import-error,no-name-in-module from restate.aws_lambda import is_running_on_lambda, wrap_asgi_as_lambda_handler +from restate._internal import PyIdentityVerifier, IdentityVerificationException # pylint: disable=import-error,no-name-in-module +from restate._internal import SDK_VERSION # pylint: disable=import-error,no-name-in-module + X_RESTATE_SERVER = header_to_binary([("x-restate-server", f"restate-sdk-python/{SDK_VERSION}")]) + async def send_status(send, receive, status_code: int): """respond with a status code""" - await send({'type': 'http.response.start', 'status': status_code, "headers": X_RESTATE_SERVER}) + await send({"type": "http.response.start", "status": status_code, "headers": X_RESTATE_SERVER}) # For more info on why this loop, see ServerInvocationContext.leave() # pylint: disable=R0801 while True: event = await receive() if event is None: break - if event.get('type') == 'http.disconnect': + if event.get("type") == "http.disconnect": break - if event.get('type') == 'http.request' and event.get('more_body', False) is False: + if event.get("type") == "http.request" and event.get("more_body", False) is False: break - await send({'type': 'http.response.body'}) + await send({"type": "http.response.body"}) + async def send404(send, receive): """respond with a 404""" await send_status(send, receive, 404) + async def send_discovery(scope: Scope, send: Send, endpoint: Endpoint): """respond with a discovery""" discovered_as: Literal["request_response", "bidi"] - if scope['http_version'] == '1.1': + if scope["http_version"] == "1.1": discovered_as = "request_response" else: discovered_as = "bidi" # Extract Accept header from request accept_header = None - for header_name, header_value in binary_to_header(scope['headers']): - if header_name.lower() == 'accept': + for header_name, header_value in binary_to_header(scope["headers"]): + if header_name.lower() == "accept": accept_header = header_value break @@ -68,95 +73,76 @@ async def send_discovery(scope: Scope, send: Send, endpoint: Endpoint): elif "application/vnd.restate.endpointmanifest.v2+json" in accept_header: version = 2 else: - await send_status_with_error_text( - send, - 415, - f"Unsupported discovery version ${accept_header}") + await send_status_with_error_text(send, 415, f"Unsupported discovery version ${accept_header}") return try: js = compute_discovery_json(endpoint, version, discovered_as) - bin_headers = header_to_binary([( - "content-type", - f"application/vnd.restate.endpointmanifest.v{version}+json")]) + bin_headers = header_to_binary([("content-type", f"application/vnd.restate.endpointmanifest.v{version}+json")]) bin_headers.extend(X_RESTATE_SERVER) - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': bin_headers, - 'trailers': False - }) - await send({ - 'type': 'http.response.body', - 'body': js.encode('utf-8'), - 'more_body': False, - }) + await send({"type": "http.response.start", "status": 200, "headers": bin_headers, "trailers": False}) + await send( + { + "type": "http.response.body", + "body": js.encode("utf-8"), + "more_body": False, + } + ) except ValueError as e: await send_status_with_error_text(send, 500, f"Error when computing discovery ${e}") return + async def send_status_with_error_text(send: Send, status_code: int, error_text: str): """respond with an health check""" headers = header_to_binary([("content-type", "text/plain")]) headers.extend(X_RESTATE_SERVER) - await send({ - 'type': 'http.response.start', - 'status': status_code, - 'headers': headers, - 'trailers': False - }) - await send({ - 'type': 'http.response.body', - 'body': error_text.encode('utf-8'), - 'more_body': False, - }) + await send({"type": "http.response.start", "status": status_code, "headers": headers, "trailers": False}) + await send( + { + "type": "http.response.body", + "body": error_text.encode("utf-8"), + "more_body": False, + } + ) + async def send_health_check(send: Send): """respond with an health check""" headers = header_to_binary([("content-type", "application/json")]) headers.extend(X_RESTATE_SERVER) - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': headers, - 'trailers': False - }) - await send({ - 'type': 'http.response.body', - 'body': b'{"status":"ok"}', - 'more_body': False, - }) - - -async def process_invocation_to_completion(vm: VMWrapper, - handler, - attempt_headers: Dict[str, str], - receive: ReceiveChannel, - send: Send): + await send({"type": "http.response.start", "status": 200, "headers": headers, "trailers": False}) + await send( + { + "type": "http.response.body", + "body": b'{"status":"ok"}', + "more_body": False, + } + ) + + +async def process_invocation_to_completion( + vm: VMWrapper, handler, attempt_headers: Dict[str, str], receive: ReceiveChannel, send: Send +): """Invoke the user code.""" status, res_headers = vm.get_response_head() res_bin_headers = header_to_binary(res_headers) res_bin_headers.extend(X_RESTATE_SERVER) - await send({ - 'type': 'http.response.start', - 'status': status, - 'headers': res_bin_headers, - 'trailers': False - }) + await send({"type": "http.response.start", "status": status, "headers": res_bin_headers, "trailers": False}) assert status == 200 # ======================================== # Read the input and the journal # ======================================== while True: message = await receive() - if message.get('type') == 'http.disconnect': + if message.get("type") == "http.disconnect": # everything ends here really ... return - if message.get('type') == 'http.request': - body = message.get('body', None) + if message.get("type") == "http.request": + body = message.get("body", None) assert isinstance(body, bytes) vm.notify_input(body) - if not message.get('more_body', False): + if not message.get("more_body", False): vm.notify_input_closed() break if vm.is_ready_to_execute(): @@ -165,12 +151,9 @@ async def process_invocation_to_completion(vm: VMWrapper, # Execute the user code # ======================================== invocation = vm.sys_input() - context = ServerInvocationContext(vm=vm, - handler=handler, - invocation=invocation, - attempt_headers=attempt_headers, - send=send, - receive=receive) + context = ServerInvocationContext( + vm=vm, handler=handler, invocation=invocation, attempt_headers=attempt_headers, send=send, receive=receive + ) try: await context.enter() except asyncio.exceptions.CancelledError: @@ -188,16 +171,19 @@ async def process_invocation_to_completion(vm: VMWrapper, finally: context.on_attempt_finished() + class LifeSpanNotImplemented(ValueError): """Signal to the asgi server that we didn't implement lifespans""" class ParsedPath(TypedDict): """Parsed path from the request.""" + type: Literal["invocation", "health", "discover", "unknown"] service: str | None handler: str | None + def parse_path(request: str) -> ParsedPath: """Parse the path from the request.""" # The following routes are possible @@ -205,21 +191,21 @@ def parse_path(request: str) -> ParsedPath: # $mountpoint/discover # $mountpoint/invoke/:service/:handler # as we don't know the mountpoint, we need to check the path carefully - fragments = request.rsplit('/', 4) + fragments = request.rsplit("/", 4) # /invoke/:service/:handler - if len(fragments) >= 3 and fragments[-3] == 'invoke': - return { "type": "invocation" , "handler" : fragments[-1], "service" : fragments[-2] } + if len(fragments) >= 3 and fragments[-3] == "invoke": + return {"type": "invocation", "handler": fragments[-1], "service": fragments[-2]} # /health - if fragments[-1] == 'health': - return { "type": "health", "service": None, "handler": None } + if fragments[-1] == "health": + return {"type": "health", "service": None, "handler": None} # /discover - if fragments[-1] == 'discover': - return { "type": "discover" , "service": None, "handler": None } + if fragments[-1] == "discover": + return {"type": "discover", "service": None, "handler": None} # anything other than invoke is 404 - return { "type": "unknown" , "service": None, "handler": None } + return {"type": "unknown", "service": None, "handler": None} -def asgi_app(endpoint: Endpoint): +def asgi_app(endpoint: Endpoint) -> RestateAppT: """Create an ASGI-3 app for the given endpoint.""" # Prepare request signer @@ -227,24 +213,24 @@ def asgi_app(endpoint: Endpoint): async def app(scope: Scope, receive: Receive, send: Send): try: - if scope['type'] == 'lifespan': + if scope["type"] == "lifespan": raise LifeSpanNotImplemented() - if scope['type'] != 'http': + if scope["type"] != "http": raise NotImplementedError(f"Unknown scope type {scope['type']}") - request_path = scope['path'] + request_path = scope["path"] assert isinstance(request_path, str) request: ParsedPath = parse_path(request_path) # Health check - if request['type'] == 'health': + if request["type"] == "health": await send_health_check(send) return # Verify Identity - assert not isinstance(scope['headers'], str) - assert hasattr(scope['headers'], '__iter__') - request_headers = binary_to_header(scope['headers']) + assert not isinstance(scope["headers"], str) + assert hasattr(scope["headers"], "__iter__") + request_headers = binary_to_header(scope["headers"]) try: identity_verifier.verify(request_headers, request_path) except IdentityVerificationException: @@ -253,17 +239,17 @@ async def app(scope: Scope, receive: Receive, send: Send): return # might be a discovery request - if request['type'] == 'discover': + if request["type"] == "discover": await send_discovery(scope, send, endpoint) return # anything other than invoke is 404 - if request['type'] == 'unknown': + if request["type"] == "unknown": await send404(send, receive) return - assert request['type'] == 'invocation' - assert request['service'] is not None - assert request['handler'] is not None - service_name, handler_name = request['service'], request['handler'] + assert request["type"] == "invocation" + assert request["service"] is not None + assert request["handler"] is not None + service_name, handler_name = request["service"], request["handler"] service = endpoint.services.get(service_name) if not service: await send404(send, receive) @@ -278,11 +264,9 @@ async def app(scope: Scope, receive: Receive, send: Send): # receive_channel = ReceiveChannel(receive) try: - await process_invocation_to_completion(VMWrapper(request_headers), - handler, - dict(request_headers), - receive_channel, - send) + await process_invocation_to_completion( + VMWrapper(request_headers), handler, dict(request_headers), receive_channel, send + ) finally: await receive_channel.close() except LifeSpanNotImplemented as e: diff --git a/python/restate/server_context.py b/python/restate/server_context.py index 1f23f1d..affbd80 100644 --- a/python/restate/server_context.py +++ b/python/restate/server_context.py @@ -29,18 +29,38 @@ from uuid import UUID import time -from restate.context import DurablePromise, AttemptFinishedEvent, HandlerType, ObjectContext, Request, RestateDurableCallFuture, RestateDurableFuture, RunAction, SendHandle, RestateDurableSleepFuture, RunOptions, P +from restate.context import ( + DurablePromise, + AttemptFinishedEvent, + HandlerType, + ObjectContext, + Request, + RestateDurableCallFuture, + RestateDurableFuture, + RunAction, + SendHandle, + RestateDurableSleepFuture, + RunOptions, + P, +) from restate.exceptions import TerminalError, SdkInternalBaseException, SdkInternalException, SuspendedException from restate.handler import Handler, handler_from_callable, invoke_handler from restate.serde import BytesSerde, DefaultSerde, JsonSerde, Serde from restate.server_types import ReceiveChannel, Send from restate.vm import Failure, Invocation, NotReady, VMWrapper, RunRetryConfig, Suspended # pylint: disable=line-too-long -from restate.vm import DoProgressAnyCompleted, DoProgressCancelSignalReceived, DoProgressReadFromInput, DoProgressExecuteRun, DoWaitPendingRun +from restate.vm import ( + DoProgressAnyCompleted, + DoProgressCancelSignalReceived, + DoProgressReadFromInput, + DoProgressExecuteRun, + DoWaitPendingRun, +) -T = TypeVar('T') -I = TypeVar('I') -O = TypeVar('O') +T = TypeVar("T") +I = TypeVar("I") +O = TypeVar("O") + class DisconnectedException(Exception): """ @@ -73,9 +93,10 @@ async def wait(self): class LazyFuture(typing.Generic[T]): """ Creates a task lazily, and allows multiple awaiters to the same coroutine. - The async_def will be executed at most 1 times. (0 if __await__ or get() not called) + The async_def will be executed at most 1 times. (0 if __await__ or get() not called) """ - __slots__ = ['async_def', 'task'] + + __slots__ = ["async_def", "task"] def __init__(self, async_def: Callable[[], typing.Coroutine[Any, Any, T]]) -> None: assert async_def is not None @@ -97,6 +118,7 @@ async def get(self) -> T: def __await__(self): return self.get().__await__() + class ServerDurableFuture(RestateDurableFuture[T]): """This class implements a durable future API""" @@ -109,7 +131,7 @@ def __init__(self, context: "ServerInvocationContext", handle: int, async_def) - def is_completed(self): """ A future is completed, either it was physically completed and its value has been collected. - OR it might not yet physically completed (i.e. the async_def didn't finish yet) BUT our VM + OR it might not yet physically completed (i.e. the async_def didn't finish yet) BUT our VM already has a completion value for it. """ return self.future.done() or self.context.vm.is_completed(self.handle) @@ -117,20 +139,20 @@ def is_completed(self): def __await__(self): return self.future.__await__() + class ServerDurableSleepFuture(RestateDurableSleepFuture, ServerDurableFuture[None]): """This class implements a durable sleep future API""" def __await__(self) -> typing.Generator[Any, Any, None]: return self.future.__await__() + class ServerCallDurableFuture(RestateDurableCallFuture[T], ServerDurableFuture[T]): """This class implements a durable future but for calls""" - def __init__(self, - context: "ServerInvocationContext", - result_handle: int, - result_async_def, - invocation_id_async_def) -> None: + def __init__( + self, context: "ServerInvocationContext", result_handle: int, result_async_def, invocation_id_async_def + ) -> None: super().__init__(context, result_handle, result_async_def) self.invocation_id_future = LazyFuture(invocation_id_async_def) @@ -150,6 +172,7 @@ async def cancel_invocation(self) -> None: inv = await self.invocation_id() self.context.cancel_invocation(inv) + class ServerSendHandle(SendHandle): """This class implements the send API""" @@ -174,10 +197,12 @@ async def cancel_invocation(self) -> None: invocation_id = await self.invocation_id() self.context.cancel_invocation(invocation_id) + async def async_value(n: Callable[[], T]) -> T: """convert a simple value to a coroutine.""" return n() + class ServerDurablePromise(DurablePromise): """This class implements a durable promise API""" @@ -232,6 +257,7 @@ def __await__(self): # disable too many public method # pylint: disable=R0904 + class Tasks: """ This class implements a list of tasks. @@ -267,24 +293,28 @@ def cancel(self): for task in to_cancel: task.cancel() -restate_context_is_replaying = contextvars.ContextVar('restate_context_is_replaying', default=False) + +restate_context_is_replaying = contextvars.ContextVar("restate_context_is_replaying", default=False) + def update_restate_context_is_replaying(vm: VMWrapper): """Update the context var 'restate_context_is_replaying'. This should be called after each vm.sys_*""" restate_context_is_replaying.set(vm.is_replaying()) + # pylint: disable=R0902 class ServerInvocationContext(ObjectContext): """This class implements the context for the restate framework based on the server.""" - def __init__(self, - vm: VMWrapper, - handler: Handler[I, O], - invocation: Invocation, - attempt_headers: Dict[str, str], - send: Send, - receive: ReceiveChannel - ) -> None: + def __init__( + self, + vm: VMWrapper, + handler: Handler[I, O], + invocation: Invocation, + attempt_headers: Dict[str, str], + send: Send, + receive: ReceiveChannel, + ) -> None: super().__init__() self.vm = vm self.handler = handler @@ -293,7 +323,7 @@ def __init__(self, self.send = send self.random_instance = Random(invocation.random_seed) self.receive = receive - self.run_coros_to_execute: dict[int, Callable[[], Awaitable[None]]] = {} + self.run_coros_to_execute: dict[int, Callable[[], Awaitable[None]]] = {} self.request_finished_event = asyncio.Event() self.tasks = Tasks() @@ -319,9 +349,13 @@ async def enter(self): except DisconnectedException: raise except Exception as e: - stacktrace = '\n'.join(traceback.format_exception(e)) - self.vm.notify_error(repr(e), stacktrace) - raise e + # check the immediate cause for SdkInternalBaseException + cause = e.__cause__ + if not isinstance(cause, SdkInternalBaseException): + # unexpected exception, notify the VM + stacktrace = "\n".join(traceback.format_exception(e)) + self.vm.notify_error(repr(e), stacktrace) + raise e async def leave(self): """Leave the context.""" @@ -329,11 +363,13 @@ async def leave(self): chunk = self.vm.take_output() if chunk is None: break - await self.send({ - 'type': 'http.response.body', - 'body': chunk, - 'more_body': True, - }) + await self.send( + { + "type": "http.response.body", + "body": chunk, + "more_body": True, + } + ) # ======================================== # End the connection # ======================================== @@ -347,11 +383,13 @@ async def leave(self): # it is important to do it, after the other side has closed his side, # because some asgi servers (like hypercorn) will remove the stream # as soon as they see a close event (in asgi terms more_body=False) - await self.send({ - 'type': 'http.response.body', - 'body': b'', - 'more_body': False, - }) + await self.send( + { + "type": "http.response.body", + "body": b"", + "more_body": False, + } + ) # notify to any holder of the abort signal that we are done def on_attempt_finished(self): @@ -368,11 +406,13 @@ async def take_and_send_output(self): """Take output from state machine and send it""" output = self.vm.take_output() if output: - await self.send({ - 'type': 'http.response.body', - 'body': bytes(output), - 'more_body': True, - }) + await self.send( + { + "type": "http.response.body", + "body": bytes(output), + "more_body": True, + } + ) async def must_take_notification(self, handle): """Take notification, which must be present""" @@ -420,26 +460,27 @@ async def wrapper(f): try: await f() finally: - await self.receive.enqueue_restate_event({ 'type' : 'restate.run_completed', 'data': None}) + await self.receive.enqueue_restate_event({"type": "restate.run_completed", "data": None}) task = asyncio.create_task(wrapper(fn)) self.tasks.add(task) continue if isinstance(do_progress_response, (DoWaitPendingRun, DoProgressReadFromInput)): chunk = await self.receive() - if chunk.get('type') == 'restate.run_completed': + if chunk.get("type") == "restate.run_completed": continue - if chunk.get('type') == 'http.disconnect': + if chunk.get("type") == "http.disconnect": raise DisconnectedException() - if chunk.get('body', None) is not None: - body = chunk.get('body') + if chunk.get("body", None) is not None: + body = chunk.get("body") assert isinstance(body, bytes) self.vm.notify_input(body) - if not chunk.get('more_body', False): + if not chunk.get("more_body", False): self.vm.notify_input_closed() def _create_fetch_result_coroutine(self, handle: int, serde: Serde[T] | None = None): """Create a coroutine that fetches a result from a notification handle.""" + async def fetch_result(): if not self.vm.is_completed(handle): await self.create_poll_or_cancel_coroutine([handle]) @@ -458,14 +499,19 @@ def create_future(self, handle: int, serde: Serde[T] | None = None) -> ServerDur def create_sleep_future(self, handle: int) -> ServerDurableSleepFuture: """Create a durable sleep future.""" + async def transform(): if not self.vm.is_completed(handle): await self.create_poll_or_cancel_coroutine([handle]) await self.must_take_notification(handle) + return ServerDurableSleepFuture(self, handle, transform) - def create_call_future(self, handle: int, invocation_id_handle: int, serde: Serde[T] | None = None) -> ServerCallDurableFuture[T]: + def create_call_future( + self, handle: int, invocation_id_handle: int, serde: Serde[T] | None = None + ) -> ServerCallDurableFuture[T]: """Create a durable future.""" + async def inv_id_factory(): if not self.vm.is_completed(invocation_id_handle): await self.create_poll_or_cancel_coroutine([invocation_id_handle]) @@ -473,15 +519,14 @@ async def inv_id_factory(): return ServerCallDurableFuture(self, handle, self._create_fetch_result_coroutine(handle, serde), inv_id_factory) - def get(self, name: str, - serde: Serde[T] = DefaultSerde(), - type_hint: Optional[typing.Type[T]] = None - ) -> Awaitable[Optional[T]]: + def get( + self, name: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> Awaitable[Optional[T]]: handle = self.vm.sys_get_state(name) update_restate_context_is_replaying(self.vm) if isinstance(serde, DefaultSerde): serde = serde.with_maybe_type(type_hint) - return self.create_future(handle, serde) # type: ignore + return self.create_future(handle, serde) # type: ignore def state_keys(self) -> Awaitable[List[str]]: handle = self.vm.sys_get_state_keys() @@ -524,21 +569,22 @@ def time(self) -> RestateDurableFuture[float]: return self.run_typed("timestamp", lambda: time.time()) # pylint: disable=R0914 - async def create_run_coroutine(self, - handle: int, - action: RunAction[T], - serde: Serde[T], - max_attempts: Optional[int] = None, - max_duration: Optional[timedelta] = None, - initial_retry_interval: Optional[timedelta] = None, - max_retry_interval: Optional[timedelta] = None, - retry_interval_factor: Optional[float] = None, - ): + async def create_run_coroutine( + self, + handle: int, + action: RunAction[T], + serde: Serde[T], + max_attempts: Optional[int] = None, + max_duration: Optional[timedelta] = None, + initial_retry_interval: Optional[timedelta] = None, + max_retry_interval: Optional[timedelta] = None, + retry_interval_factor: Optional[float] = None, + ) -> None: """Create a coroutine to poll the handle.""" start = time.time() try: if inspect.iscoroutinefunction(action): - action_result: T = await action() # type: ignore + action_result: T = await action() # type: ignore else: loop = asyncio.get_running_loop() ctx = contextvars.copy_context() @@ -562,28 +608,35 @@ async def create_run_coroutine(self, attempt_duration = int((end - start) * 1000) failure = Failure(code=500, message=str(e)) max_duration_ms = None if max_duration is None else int(max_duration.total_seconds() * 1000) - initial_retry_interval_ms = None if initial_retry_interval is None else int(initial_retry_interval.total_seconds() * 1000) - max_retry_interval_ms = None if max_retry_interval is None else int(max_retry_interval.total_seconds() * 1000) + initial_retry_interval_ms = ( + None if initial_retry_interval is None else int(initial_retry_interval.total_seconds() * 1000) + ) + max_retry_interval_ms = ( + None if max_retry_interval is None else int(max_retry_interval.total_seconds() * 1000) + ) config = RunRetryConfig( max_attempts=max_attempts, max_duration=max_duration_ms, initial_interval=initial_retry_interval_ms, max_interval=max_retry_interval_ms, - interval_factor=retry_interval_factor + interval_factor=retry_interval_factor, + ) + self.vm.propose_run_completion_transient( + handle, failure=failure, attempt_duration_ms=attempt_duration, config=config ) - self.vm.propose_run_completion_transient(handle, failure=failure, attempt_duration_ms=attempt_duration, config=config) + # pylint: disable=W0236 # pylint: disable=R0914 - def run(self, - name: str, - action: RunAction[T], - serde: Serde[T] = DefaultSerde(), - max_attempts: Optional[int] = None, - max_retry_duration: Optional[timedelta] = None, - type_hint: Optional[typing.Type[T]] = None, - args: Optional[typing.Tuple[Any, ...]] = None - ) -> RestateDurableFuture[T]: - + def run( + self, + name: str, + action: RunAction[T], + serde: Serde[T] = DefaultSerde(), + max_attempts: Optional[int] = None, + max_retry_duration: Optional[timedelta] = None, + type_hint: Optional[typing.Type[T]] = None, + args: Optional[typing.Tuple[Any, ...]] = None, + ) -> RestateDurableFuture[T]: if isinstance(serde, DefaultSerde): if type_hint is None: signature = inspect.signature(action, eval_str=True) @@ -594,17 +647,18 @@ def run(self, update_restate_context_is_replaying(self.vm) if args is not None: - noargs_action = functools.partial(action, *args) + noargs_action = typing.cast(RunAction[T], functools.partial(action, *args)) else: - # todo: we can also verify by looking at the signature that there are no missing parameters - noargs_action = action # type: ignore - self.run_coros_to_execute[handle] = lambda : self.create_run_coroutine(handle, noargs_action, serde, max_attempts, max_retry_duration, None, None, None) - return self.create_future(handle, serde) # type: ignore + noargs_action = action + self.run_coros_to_execute[handle] = lambda: self.create_run_coroutine( + handle, noargs_action, serde, max_attempts, max_retry_duration, None, None, None + ) + return self.create_future(handle, serde) # type: ignore def run_typed( self, name: str, - action: Union[Callable[P, T], Callable[P, Coroutine[Any, Any, T]]], + action: Union[Callable[P, T], Callable[P, Coroutine[Any, Any, T]]], options: RunOptions[T] = RunOptions(), /, *args: P.args, @@ -620,8 +674,8 @@ def run_typed( handle = self.vm.sys_run(name) update_restate_context_is_replaying(self.vm) - func = functools.partial(action, *args, **kwargs) - self.run_coros_to_execute[handle] = lambda : self.create_run_coroutine( + func = typing.cast(RunAction[T], functools.partial(action, *args, **kwargs)) + self.run_coros_to_execute[handle] = lambda: self.create_run_coroutine( handle, func, options.serde, @@ -629,7 +683,7 @@ def run_typed( options.max_duration, options.initial_retry_interval, options.max_retry_interval, - options.retry_interval_factor + options.retry_interval_factor, ) return self.create_future(handle, options.serde) @@ -638,38 +692,41 @@ def sleep(self, delta: timedelta, name: Optional[str] = None) -> RestateDurableS millis = int(delta.total_seconds() * 1000) handle = self.vm.sys_sleep(millis, name) update_restate_context_is_replaying(self.vm) - return self.create_sleep_future(handle) # type: ignore - - def do_call(self, - tpe: HandlerType[I, O], - parameter: I, - key: Optional[str] = None, - send_delay: Optional[timedelta] = None, - send: bool = False, - idempotency_key: str | None = None, - headers: typing.Dict[str,str] | None = None - ) -> RestateDurableCallFuture[O] | SendHandle: + return self.create_sleep_future(handle) # type: ignore + + def do_call( + self, + tpe: HandlerType[I, O], + parameter: I, + key: Optional[str] = None, + send_delay: Optional[timedelta] = None, + send: bool = False, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O] | SendHandle: """Make an RPC call to the given handler""" target_handler = handler_from_callable(tpe) - service=target_handler.service_tag.name - handler=target_handler.name + service = target_handler.service_tag.name + handler = target_handler.name input_serde = target_handler.handler_io.input_serde output_serde = target_handler.handler_io.output_serde - return self.do_raw_call(service, handler, parameter, input_serde, output_serde, key, send_delay, send, idempotency_key, headers) - - - def do_raw_call(self, - service: str, - handler:str, - input_param: I, - input_serde: Serde[I], - output_serde: Serde[O], - key: Optional[str] = None, - send_delay: Optional[timedelta] = None, - send: bool = False, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O] | SendHandle: + return self.do_raw_call( + service, handler, parameter, input_serde, output_serde, key, send_delay, send, idempotency_key, headers + ) + + def do_raw_call( + self, + service: str, + handler: str, + input_param: I, + input_serde: Serde[I], + output_serde: Serde[O], + key: Optional[str] = None, + send_delay: Optional[timedelta] = None, + send: bool = False, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O] | SendHandle: """Make an RPC call to the given handler""" parameter = input_serde.serialize(input_param) if headers is not None: @@ -678,114 +735,172 @@ def do_raw_call(self, headers_kvs = [] if send_delay: ms = int(send_delay.total_seconds() * 1000) - send_handle = self.vm.sys_send(service, handler, parameter, key, delay=ms, idempotency_key=idempotency_key, headers=headers_kvs) + send_handle = self.vm.sys_send( + service, handler, parameter, key, delay=ms, idempotency_key=idempotency_key, headers=headers_kvs + ) update_restate_context_is_replaying(self.vm) return ServerSendHandle(self, send_handle) if send: - send_handle = self.vm.sys_send(service, handler, parameter, key, idempotency_key=idempotency_key, headers=headers_kvs) + send_handle = self.vm.sys_send( + service, handler, parameter, key, idempotency_key=idempotency_key, headers=headers_kvs + ) update_restate_context_is_replaying(self.vm) return ServerSendHandle(self, send_handle) - handle = self.vm.sys_call(service=service, - handler=handler, - parameter=parameter, - key=key, - idempotency_key=idempotency_key, - headers=headers_kvs) + handle = self.vm.sys_call( + service=service, + handler=handler, + parameter=parameter, + key=key, + idempotency_key=idempotency_key, + headers=headers_kvs, + ) update_restate_context_is_replaying(self.vm) - return self.create_call_future(handle=handle.result_handle, - invocation_id_handle=handle.invocation_id_handle, - serde=output_serde) + return self.create_call_future( + handle=handle.result_handle, invocation_id_handle=handle.invocation_id_handle, serde=output_serde + ) - def service_call(self, - tpe: HandlerType[I, O], - arg: I, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O]: + def service_call( + self, + tpe: HandlerType[I, O], + arg: I, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O]: coro = self.do_call(tpe, arg, idempotency_key=idempotency_key, headers=headers) assert not isinstance(coro, SendHandle) return coro - - def service_send(self, tpe: HandlerType[I, O], arg: I, send_delay: timedelta | None = None, idempotency_key: str | None = None, headers: typing.Dict[str, str] | None = None) -> SendHandle: - send = self.do_call(tpe=tpe, parameter=arg, send_delay=send_delay, send=True, idempotency_key=idempotency_key, headers=headers) + def service_send( + self, + tpe: HandlerType[I, O], + arg: I, + send_delay: timedelta | None = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: + send = self.do_call( + tpe=tpe, parameter=arg, send_delay=send_delay, send=True, idempotency_key=idempotency_key, headers=headers + ) assert isinstance(send, SendHandle) return send - def object_call(self, - tpe: HandlerType[I, O], - key: str, - arg: I, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O]: + def object_call( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O]: coro = self.do_call(tpe, arg, key, idempotency_key=idempotency_key, headers=headers) assert not isinstance(coro, SendHandle) return coro - def object_send(self, tpe: HandlerType[I, O], key: str, arg: I, send_delay: timedelta | None = None, idempotency_key: str | None = None, headers: typing.Dict[str, str] | None = None) -> SendHandle: - send = self.do_call(tpe=tpe, key=key, parameter=arg, send_delay=send_delay, send=True, idempotency_key=idempotency_key, headers=headers) + def object_send( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + send_delay: timedelta | None = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: + send = self.do_call( + tpe=tpe, + key=key, + parameter=arg, + send_delay=send_delay, + send=True, + idempotency_key=idempotency_key, + headers=headers, + ) assert isinstance(send, SendHandle) return send - def workflow_call(self, - tpe: HandlerType[I, O], - key: str, - arg: I, - idempotency_key: str | None = None, - headers: typing.Dict[str, str] | None = None - ) -> RestateDurableCallFuture[O]: + def workflow_call( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[O]: return self.object_call(tpe, key, arg, idempotency_key=idempotency_key, headers=headers) - def workflow_send(self, tpe: HandlerType[I, O], key: str, arg: I, send_delay: timedelta | None = None, idempotency_key: str | None = None, headers: typing.Dict[str, str] | None = None) -> SendHandle: + def workflow_send( + self, + tpe: HandlerType[I, O], + key: str, + arg: I, + send_delay: timedelta | None = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: send = self.object_send(tpe, key, arg, send_delay, idempotency_key=idempotency_key, headers=headers) assert isinstance(send, SendHandle) return send - def generic_call(self, service: str, handler: str, arg: bytes, key: str | None = None, idempotency_key: str | None = None, headers: typing.Dict[str, str] | None = None) -> RestateDurableCallFuture[bytes]: + def generic_call( + self, + service: str, + handler: str, + arg: bytes, + key: str | None = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> RestateDurableCallFuture[bytes]: serde = BytesSerde() - call_handle = self.do_raw_call(service=service, - handler=handler, - input_param=arg, - input_serde=serde, - output_serde=serde, - key=key, - idempotency_key=idempotency_key, - headers=headers) + call_handle = self.do_raw_call( + service=service, + handler=handler, + input_param=arg, + input_serde=serde, + output_serde=serde, + key=key, + idempotency_key=idempotency_key, + headers=headers, + ) assert not isinstance(call_handle, SendHandle) return call_handle - def generic_send(self, service: str, handler: str, arg: bytes, key: str | None = None, send_delay: timedelta | None = None, idempotency_key: str | None = None, headers: typing.Dict[str, str] | None = None) -> SendHandle: + def generic_send( + self, + service: str, + handler: str, + arg: bytes, + key: str | None = None, + send_delay: timedelta | None = None, + idempotency_key: str | None = None, + headers: typing.Dict[str, str] | None = None, + ) -> SendHandle: serde = BytesSerde() - send_handle = self.do_raw_call(service=service, - handler=handler, - input_param=arg, - input_serde=serde, - output_serde=serde, - key=key, - send_delay=send_delay, - send=True, - idempotency_key=idempotency_key, - headers=headers) + send_handle = self.do_raw_call( + service=service, + handler=handler, + input_param=arg, + input_serde=serde, + output_serde=serde, + key=key, + send_delay=send_delay, + send=True, + idempotency_key=idempotency_key, + headers=headers, + ) assert isinstance(send_handle, SendHandle) return send_handle - def awakeable(self, - serde: Serde[I] = DefaultSerde(), - type_hint: Optional[typing.Type[I]] = None - ) -> typing.Tuple[str, RestateDurableFuture[Any]]: + def awakeable( + self, serde: Serde[I] = DefaultSerde(), type_hint: Optional[typing.Type[I]] = None + ) -> typing.Tuple[str, RestateDurableFuture[Any]]: if isinstance(serde, DefaultSerde): serde = serde.with_maybe_type(type_hint) name, handle = self.vm.sys_awakeable() update_restate_context_is_replaying(self.vm) return name, self.create_future(handle, serde) - def resolve_awakeable(self, - name: str, - value: I, - serde: Serde[I] = DefaultSerde()) -> None: + def resolve_awakeable(self, name: str, value: I, serde: Serde[I] = DefaultSerde()) -> None: if isinstance(serde, DefaultSerde): serde = serde.with_maybe_type(type(value)) buf = serde.serialize(value) @@ -796,7 +911,9 @@ def reject_awakeable(self, name: str, failure_message: str, failure_code: int = self.vm.sys_reject_awakeable(name, Failure(code=failure_code, message=failure_message)) update_restate_context_is_replaying(self.vm) - def promise(self, name: str, serde: typing.Optional[Serde[T]] = JsonSerde(), type_hint: Optional[typing.Type[T]] = None) -> DurablePromise[T]: + def promise( + self, name: str, serde: typing.Optional[Serde[T]] = JsonSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> DurablePromise[T]: """Create a durable promise.""" if isinstance(serde, DefaultSerde): serde = serde.with_maybe_type(type_hint) @@ -812,9 +929,9 @@ def cancel_invocation(self, invocation_id: str): self.vm.sys_cancel(invocation_id) update_restate_context_is_replaying(self.vm) - def attach_invocation(self, invocation_id: str, serde: Serde[T] = DefaultSerde(), - type_hint: Optional[typing.Type[T]] = None - ) -> RestateDurableFuture[T]: + def attach_invocation( + self, invocation_id: str, serde: Serde[T] = DefaultSerde(), type_hint: Optional[typing.Type[T]] = None + ) -> RestateDurableFuture[T]: if invocation_id is None: raise ValueError("invocation_id cannot be None") if isinstance(serde, DefaultSerde): diff --git a/python/restate/server_types.py b/python/restate/server_types.py index 1045e76..63bb9ce 100644 --- a/python/restate/server_types.py +++ b/python/restate/server_types.py @@ -15,16 +15,19 @@ """ import asyncio -from typing import (Awaitable, Callable, Dict, Iterable, List, - Tuple, Union, TypedDict, Literal, Optional, Any) +from typing import Awaitable, Callable, Dict, Iterable, List, Tuple, Union, TypedDict, Literal, Optional, Any + class ASGIVersions(TypedDict): """ASGI Versions""" + spec_version: str version: Union[Literal["2.0"], Literal["3.0"]] -class Scope(TypedDict, total=False): + +class Scope(TypedDict): """ASGI Scope""" + type: Literal["http"] asgi: ASGIVersions http_version: str @@ -39,26 +42,34 @@ class Scope(TypedDict, total=False): server: Optional[Tuple[str, Optional[int]]] extensions: Optional[Dict[str, Dict[object, object]]] + class RestateEvent(TypedDict): """An event that represents a run completion""" + type: Literal["restate.run_completed"] data: Optional[Dict[str, Any]] + class HTTPRequestEvent(TypedDict): """ASGI Request event""" + type: Literal["http.request"] body: bytes more_body: bool + class HTTPResponseStartEvent(TypedDict): """ASGI Response start event""" + type: Literal["http.response.start"] status: int headers: Iterable[Tuple[bytes, bytes]] trailers: bool + class HTTPResponseBodyEvent(TypedDict): """ASGI Response body event""" + type: Literal["http.response.body"] body: bytes more_body: bool @@ -67,10 +78,7 @@ class HTTPResponseBodyEvent(TypedDict): ASGIReceiveEvent = HTTPRequestEvent -ASGISendEvent = Union[ - HTTPResponseStartEvent, - HTTPResponseBodyEvent -] +ASGISendEvent = Union[HTTPResponseStartEvent, HTTPResponseBodyEvent] Receive = Callable[[], Awaitable[ASGIReceiveEvent]] Send = Callable[[ASGISendEvent], Awaitable[None]] @@ -84,13 +92,48 @@ class HTTPResponseBodyEvent(TypedDict): Awaitable[None], ] + +class RestateLambdaRequest(TypedDict): + """ + Restate Lambda request + + :see: https://github.com/restatedev/restate/blob/1a10c05b16b387191060b49faffb0335ee97e96d/crates/service-client/src/lambda.rs#L297 # pylint: disable=line-too-long + """ + + path: str + httpMethod: str + headers: Dict[str, str] + body: str + isBase64Encoded: bool + + +class RestateLambdaResponse(TypedDict): + """ + Restate Lambda response + + :see: https://github.com/restatedev/restate/blob/1a10c05b16b387191060b49faffb0335ee97e96d/crates/service-client/src/lambda.rs#L310 # pylint: disable=line-too-long + """ + + statusCode: int + headers: Dict[str, str] + body: str + isBase64Encoded: bool + + +RestateLambdaHandler = Callable[[RestateLambdaRequest, Any], RestateLambdaResponse] + +RestateAppT = Any # Union[ASGIApp, RestateLambdaHandler] + + def header_to_binary(headers: Iterable[Tuple[str, str]]) -> List[Tuple[bytes, bytes]]: """Convert a list of headers to a list of binary headers.""" - return [ (k.encode('utf-8'), v.encode('utf-8')) for k,v in headers ] + return [(k.encode("utf-8"), v.encode("utf-8")) for k, v in headers] + def binary_to_header(headers: Iterable[Tuple[bytes, bytes]]) -> List[Tuple[str, str]]: """Convert a list of binary headers to a list of headers.""" - return [ (k.decode('utf-8'), v.decode('utf-8')) for k,v in headers ] + return [(k.decode("utf-8"), v.decode("utf-8")) for k, v in headers] + class ReceiveChannel: """ASGI receive channel.""" @@ -104,9 +147,9 @@ async def loop(): """Receive loop.""" while not self._disconnected.is_set(): event = await receive() - if event.get('type') == 'http.request' and not event.get('more_body', False): + if event.get("type") == "http.request" and not event.get("more_body", False): self._http_input_closed.set() - elif event.get('type') == 'http.disconnect': + elif event.get("type") == "http.disconnect": self._http_input_closed.set() self._disconnected.set() await self._queue.put(event) diff --git a/python/restate/service.py b/python/restate/service.py index 28b7f7c..3dbfba4 100644 --- a/python/restate/service.py +++ b/python/restate/service.py @@ -24,9 +24,9 @@ from restate.retry_policy import InvocationRetryPolicy from .handler import Handler, HandlerIO, ServiceTag, make_handler -I = typing.TypeVar('I') -O = typing.TypeVar('O') - +I = typing.TypeVar("I") +O = typing.TypeVar("O") +T = typing.TypeVar("T") # disable too many arguments warning # pylint: disable=R0913 @@ -34,6 +34,7 @@ # disable line too long warning # pylint: disable=C0301,R0902 + class Service: """ Represents a restate service. @@ -69,15 +70,18 @@ class Service: invocation_retry_policy (InvocationRetryPolicy, optional): Retry policy applied for all invocations to this service. """ - def __init__(self, name: str, - description: typing.Optional[str] = None, - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None) -> None: + def __init__( + self, + name: str, + description: typing.Optional[str] = None, + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ) -> None: self.service_tag = ServiceTag("service", name, description, metadata) self.handlers: typing.Dict[str, Handler] = {} self.inactivity_timeout = inactivity_timeout @@ -94,25 +98,26 @@ def name(self): """ return self.service_tag.name - def handler(self, - name: typing.Optional[str] = None, - accept: str = "application/json", - content_type: str = "application/json", - input_serde: Serde[I] = DefaultSerde(), - output_serde: Serde[O] = DefaultSerde(), - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None) -> typing.Callable: - + def handler( + self, + name: typing.Optional[str] = None, + accept: str = "application/json", + content_type: str = "application/json", + input_serde: Serde[I] = DefaultSerde(), + output_serde: Serde[O] = DefaultSerde(), + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ) -> typing.Callable[[T], T]: """ Decorator for defining a handler function. Args: - name: The name of the handler. + name: The name of the handler. accept: Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g. `application/*` or `*/*`. Default "application/json". content_type: The content type of the request. Default "application/json". @@ -157,17 +162,33 @@ def my_handler_func(ctx, request): # handler logic pass """ - handler_io = HandlerIO[I,O](accept, content_type, input_serde, output_serde) + handler_io = HandlerIO[I, O](accept, content_type, input_serde, output_serde) + def wrapper(fn): @wraps(fn) def wrapped(*args, **kwargs): return fn(*args, **kwargs) signature = inspect.signature(fn, eval_str=True) - handler = make_handler(self.service_tag, handler_io, name, None, wrapped, signature, inspect.getdoc(fn), metadata, - inactivity_timeout, abort_timeout, journal_retention, idempotency_retention, - None, None, ingress_private, invocation_retry_policy) + handler = make_handler( + self.service_tag, + handler_io, + name, + None, + wrapped, + signature, + inspect.getdoc(fn), + metadata, + inactivity_timeout, + abort_timeout, + journal_retention, + idempotency_retention, + None, + None, + ingress_private, + invocation_retry_policy, + ) self.handlers[handler.name] = handler return wrapped - return wrapper + return typing.cast(typing.Callable[[T], T], wrapper) diff --git a/python/restate/types.py b/python/restate/types.py new file mode 100644 index 0000000..53390ab --- /dev/null +++ b/python/restate/types.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH +# +# This file is part of the Restate SDK for Python, +# which is released under the MIT license. +# +# You can find a copy of the license in file LICENSE in the root +# directory of this repository or package, or at +# https://github.com/restatedev/sdk-typescript/blob/main/LICENSE +# + +# pylint: disable=R0917 +# pylint: disable=R0801 +""" +This represents common types used throughout the Restate SDK for Python. +""" + +from dataclasses import dataclass + + +@dataclass +class TestHarnessEnvironment: + """Information about the test environment""" + + ingress_url: str + """The URL of the Restate ingress endpoint used in the test""" + + admin_api_url: str + """The URL of the Restate admin API endpoint used in the test""" diff --git a/python/restate/vm.py b/python/restate/vm.py index 6edd02a..95951c8 100644 --- a/python/restate/vm.py +++ b/python/restate/vm.py @@ -17,19 +17,37 @@ from dataclasses import dataclass import typing -from restate._internal import PyVM, PyHeader, PyFailure, VMException, PySuspended, PyVoid, PyStateKeys, PyExponentialRetryConfig, PyDoProgressAnyCompleted, PyDoProgressReadFromInput, PyDoProgressExecuteRun, PyDoWaitForPendingRun, PyDoProgressCancelSignalReceived, CANCEL_NOTIFICATION_HANDLE # pylint: disable=import-error,no-name-in-module,line-too-long +from restate._internal import ( + PyVM, + PyHeader, + PyFailure, + VMException, + PySuspended, + PyVoid, + PyStateKeys, + PyExponentialRetryConfig, + PyDoProgressAnyCompleted, + PyDoProgressReadFromInput, + PyDoProgressExecuteRun, + PyDoWaitForPendingRun, + PyDoProgressCancelSignalReceived, + CANCEL_NOTIFICATION_HANDLE, +) # pylint: disable=import-error,no-name-in-module,line-too-long + @dataclass class Invocation: """ Invocation dataclass """ + invocation_id: str random_seed: int headers: typing.List[typing.Tuple[str, str]] input_buffer: bytes key: str + @dataclass class RunRetryConfig: """ @@ -37,77 +55,93 @@ class RunRetryConfig: All duration/interval values are in milliseconds. """ + initial_interval: typing.Optional[int] = None max_attempts: typing.Optional[int] = None max_duration: typing.Optional[int] = None max_interval: typing.Optional[int] = None interval_factor: typing.Optional[float] = None + @dataclass class Failure: """ Failure """ + code: int message: str + @dataclass class NotReady: """ NotReady """ + NOT_READY = NotReady() CANCEL_HANDLE = CANCEL_NOTIFICATION_HANDLE NotificationType = typing.Optional[typing.Union[bytes, Failure, NotReady, list[str], str]] + class Suspended: """ Represents a suspended error """ + SUSPENDED = Suspended() + class DoProgressAnyCompleted: """ Represents a notification that any of the handles has completed. """ + class DoProgressReadFromInput: """ Represents a notification that the input needs to be read. """ + class DoProgressExecuteRun: """ Represents a notification that a run needs to be executed. """ + handle: int def __init__(self, handle): self.handle = handle + class DoProgressCancelSignalReceived: """ Represents a notification that a cancel signal has been received """ + class DoWaitPendingRun: """ Represents a notification that a run is pending """ + DO_PROGRESS_ANY_COMPLETED = DoProgressAnyCompleted() DO_PROGRESS_READ_FROM_INPUT = DoProgressReadFromInput() DO_PROGRESS_CANCEL_SIGNAL_RECEIVED = DoProgressCancelSignalReceived() DO_WAIT_PENDING_RUN = DoWaitPendingRun() -DoProgressResult = typing.Union[DoProgressAnyCompleted, - DoProgressReadFromInput, - DoProgressExecuteRun, - DoProgressCancelSignalReceived, - DoWaitPendingRun] +DoProgressResult = typing.Union[ + DoProgressAnyCompleted, + DoProgressReadFromInput, + DoProgressExecuteRun, + DoProgressCancelSignalReceived, + DoWaitPendingRun, +] # pylint: disable=too-many-public-methods @@ -155,8 +189,7 @@ def is_completed(self, handle: int) -> bool: return self.vm.is_completed(handle) # pylint: disable=R0911 - def do_progress(self, handles: list[int]) \ - -> typing.Union[DoProgressResult, Exception, Suspended]: + def do_progress(self, handles: list[int]) -> typing.Union[DoProgressResult, Exception, Suspended]: """Do progress with notifications.""" try: result = self.vm.do_progress(handles) @@ -176,8 +209,7 @@ def do_progress(self, handles: list[int]) \ return DO_WAIT_PENDING_RUN return ValueError(f"Unknown progress type: {result}") - def take_notification(self, handle: int) \ - -> typing.Union[NotificationType, Exception, Suspended]: + def take_notification(self, handle: int) -> typing.Union[NotificationType, Exception, Suspended]: """Take the result of an asynchronous operation.""" try: result = self.vm.take_notification(handle) @@ -208,10 +240,10 @@ def take_notification(self, handle: int) \ def sys_input(self) -> Invocation: """ - Retrieves the system input from the virtual machine. + Retrieves the system input from the virtual machine. - Returns: - An instance of the Invocation class containing the system input. + Returns: + An instance of the Invocation class containing the system input. """ inp = self.vm.sys_input() invocation_id: str = inp.invocation_id @@ -221,11 +253,8 @@ def sys_input(self) -> Invocation: key: str = inp.key return Invocation( - invocation_id=invocation_id, - random_seed=random_seed, - headers=headers, - input_buffer=input_buffer, - key=key) + invocation_id=invocation_id, random_seed=random_seed, headers=headers, input_buffer=input_buffer, key=key + ) def sys_write_output_success(self, output: bytes): """ @@ -252,7 +281,6 @@ def sys_write_output_failure(self, output: Failure): res = PyFailure(output.code, output.message) self.vm.sys_write_output_failure(res) - def sys_get_state(self, name) -> int: """ Retrieves a key-value binding. @@ -265,7 +293,6 @@ def sys_get_state(self, name) -> int: """ return self.vm.sys_get_state(name) - def sys_get_state_keys(self) -> int: """ Retrieves all keys. @@ -275,7 +302,6 @@ def sys_get_state_keys(self) -> int: """ return self.vm.sys_get_state_keys() - def sys_set_state(self, name: str, value: bytes): """ Sets a key-value binding. @@ -301,29 +327,31 @@ def sys_sleep(self, millis: int, name: typing.Optional[str] = None): """Ask to sleep for a given duration""" return self.vm.sys_sleep(millis, name) - def sys_call(self, - service: str, - handler: str, - parameter: bytes, - key: typing.Optional[str] = None, - idempotency_key: typing.Optional[str] = None, - headers: typing.Optional[typing.List[typing.Tuple[str, str]]] = None - ): + def sys_call( + self, + service: str, + handler: str, + parameter: bytes, + key: typing.Optional[str] = None, + idempotency_key: typing.Optional[str] = None, + headers: typing.Optional[typing.List[typing.Tuple[str, str]]] = None, + ): """Call a service""" if headers: headers = [PyHeader(key=h[0], value=h[1]) for h in headers] return self.vm.sys_call(service, handler, parameter, key, idempotency_key, headers) # pylint: disable=too-many-arguments - def sys_send(self, - service: str, - handler: str, - parameter: bytes, - key: typing.Optional[str] = None, - delay: typing.Optional[int] = None, - idempotency_key: typing.Optional[str] = None, - headers: typing.Optional[typing.List[typing.Tuple[str, str]]] = None - ) -> int: + def sys_send( + self, + service: str, + handler: str, + parameter: bytes, + key: typing.Optional[str] = None, + delay: typing.Optional[int] = None, + idempotency_key: typing.Optional[str] = None, + headers: typing.Optional[typing.List[typing.Tuple[str, str]]] = None, + ) -> int: """ send an invocation to a service, and return the handle to the promise that will resolve with the invocation id @@ -398,7 +426,9 @@ def propose_run_completion_failure(self, handle: int, output: Failure) -> int: return self.vm.propose_run_completion_failure(handle, res) # pylint: disable=line-too-long - def propose_run_completion_transient(self, handle: int, failure: Failure, attempt_duration_ms: int, config: RunRetryConfig): + def propose_run_completion_transient( + self, handle: int, failure: Failure, attempt_duration_ms: int, config: RunRetryConfig + ): """ Exit a side effect with a transient Error. This requires a retry policy to be provided. @@ -409,7 +439,7 @@ def propose_run_completion_transient(self, handle: int, failure: Failure, attemp config.max_attempts, config.max_duration, config.max_interval, - config.interval_factor + config.interval_factor, ) self.vm.propose_run_completion_failure_transient(handle, py_failure, attempt_duration_ms, py_config) diff --git a/python/restate/workflow.py b/python/restate/workflow.py index b937879..17058fa 100644 --- a/python/restate/workflow.py +++ b/python/restate/workflow.py @@ -24,8 +24,9 @@ from restate.serde import DefaultSerde, Serde from restate.handler import Handler, HandlerIO, ServiceTag, make_handler -I = typing.TypeVar('I') -O = typing.TypeVar('O') +I = typing.TypeVar("I") +O = typing.TypeVar("O") +T = typing.TypeVar("T") # disable too many arguments warning @@ -37,6 +38,7 @@ # disable similar lines warning # pylint: disable=R0801 + # pylint: disable=R0902 class Workflow: """ @@ -78,17 +80,19 @@ class Workflow: handlers: typing.Dict[str, Handler[typing.Any, typing.Any]] - def __init__(self, - name, - description: typing.Optional[str] = None, - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - enable_lazy_state: typing.Optional[bool] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None): + def __init__( + self, + name, + description: typing.Optional[str] = None, + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + enable_lazy_state: typing.Optional[bool] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ): self.service_tag = ServiceTag("workflow", name, description, metadata) self.handlers = {} self.inactivity_timeout = inactivity_timeout @@ -106,25 +110,27 @@ def name(self): """ return self.service_tag.name - def main(self, - name: typing.Optional[str] = None, - accept: str = "application/json", - content_type: str = "application/json", - input_serde: Serde[I] = DefaultSerde[I](), # type: ignore - output_serde: Serde[O] = DefaultSerde[O](), # type: ignore - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - workflow_retention: typing.Optional[timedelta] = None, - enable_lazy_state: typing.Optional[bool] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None) -> typing.Callable: # type: ignore + def main( + self, + name: typing.Optional[str] = None, + accept: str = "application/json", + content_type: str = "application/json", + input_serde: Serde[I] = DefaultSerde[I](), # type: ignore + output_serde: Serde[O] = DefaultSerde[O](), # type: ignore + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + workflow_retention: typing.Optional[timedelta] = None, + enable_lazy_state: typing.Optional[bool] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ) -> typing.Callable[[T], T]: """ Mark this handler as a workflow entry point. Args: - name: The name of the handler. + name: The name of the handler. accept: Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g. `application/*` or `*/*`. Default "application/json". content_type: The content type of the request. Default "application/json". @@ -160,41 +166,45 @@ def main(self, otherwise the service discovery will fail. invocation_retry_policy (InvocationRetryPolicy, optional): Retry policy applied for all invocations to this workflow. """ - return self._add_handler(name, - kind="workflow", - accept=accept, - content_type=content_type, - input_serde=input_serde, - output_serde=output_serde, - metadata=metadata, - inactivity_timeout=inactivity_timeout, - abort_timeout=abort_timeout, - journal_retention=journal_retention, - idempotency_retention=None, - workflow_retention=workflow_retention, - enable_lazy_state=enable_lazy_state, - ingress_private=ingress_private, - invocation_retry_policy=invocation_retry_policy) + return self._add_handler( + name, + kind="workflow", + accept=accept, + content_type=content_type, + input_serde=input_serde, + output_serde=output_serde, + metadata=metadata, + inactivity_timeout=inactivity_timeout, + abort_timeout=abort_timeout, + journal_retention=journal_retention, + idempotency_retention=None, + workflow_retention=workflow_retention, + enable_lazy_state=enable_lazy_state, + ingress_private=ingress_private, + invocation_retry_policy=invocation_retry_policy, + ) - def handler(self, - name: typing.Optional[str] = None, - accept: str = "application/json", - content_type: str = "application/json", - input_serde: Serde[I] = DefaultSerde[I](), # type: ignore - output_serde: Serde[O] = DefaultSerde[O](), # type: ignore - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - enable_lazy_state: typing.Optional[bool] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None) -> typing.Callable: + def handler( + self, + name: typing.Optional[str] = None, + accept: str = "application/json", + content_type: str = "application/json", + input_serde: Serde[I] = DefaultSerde[I](), # type: ignore + output_serde: Serde[O] = DefaultSerde[O](), # type: ignore + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + enable_lazy_state: typing.Optional[bool] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional[InvocationRetryPolicy] = None, + ) -> typing.Callable[[T], T]: """ Decorator for defining a handler function. Args: - name: The name of the handler. + name: The name of the handler. accept: Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g. `application/*` or `*/*`. Default "application/json". content_type: The content type of the request. Default "application/json". @@ -230,32 +240,48 @@ def handler(self, otherwise the service discovery will fail. invocation_retry_policy (InvocationRetryPolicy, optional): Retry policy applied for all invocations to this handler. """ - return self._add_handler(name, "shared", accept, content_type, input_serde, output_serde, metadata, - inactivity_timeout, abort_timeout, journal_retention, idempotency_retention, - None, enable_lazy_state, ingress_private, invocation_retry_policy) + return self._add_handler( + name, + "shared", + accept, + content_type, + input_serde, + output_serde, + metadata, + inactivity_timeout, + abort_timeout, + journal_retention, + idempotency_retention, + None, + enable_lazy_state, + ingress_private, + invocation_retry_policy, + ) # pylint: disable=R0914 - def _add_handler(self, - name: typing.Optional[str] = None, - kind: typing.Literal["workflow", "shared", "exclusive"] = "shared", - accept: str = "application/json", - content_type: str = "application/json", - input_serde: Serde[I] = DefaultSerde[I](), # type: ignore - output_serde: Serde[O] = DefaultSerde[O](), # type: ignore - metadata: typing.Optional[typing.Dict[str, str]] = None, - inactivity_timeout: typing.Optional[timedelta] = None, - abort_timeout: typing.Optional[timedelta] = None, - journal_retention: typing.Optional[timedelta] = None, - idempotency_retention: typing.Optional[timedelta] = None, - workflow_retention: typing.Optional[timedelta] = None, - enable_lazy_state: typing.Optional[bool] = None, - ingress_private: typing.Optional[bool] = None, - invocation_retry_policy: typing.Optional["InvocationRetryPolicy"] = None) -> typing.Callable: # type: ignore + def _add_handler( + self, + name: typing.Optional[str] = None, + kind: typing.Literal["workflow", "shared", "exclusive"] = "shared", + accept: str = "application/json", + content_type: str = "application/json", + input_serde: Serde[I] = DefaultSerde[I](), # type: ignore + output_serde: Serde[O] = DefaultSerde[O](), # type: ignore + metadata: typing.Optional[typing.Dict[str, str]] = None, + inactivity_timeout: typing.Optional[timedelta] = None, + abort_timeout: typing.Optional[timedelta] = None, + journal_retention: typing.Optional[timedelta] = None, + idempotency_retention: typing.Optional[timedelta] = None, + workflow_retention: typing.Optional[timedelta] = None, + enable_lazy_state: typing.Optional[bool] = None, + ingress_private: typing.Optional[bool] = None, + invocation_retry_policy: typing.Optional["InvocationRetryPolicy"] = None, + ) -> typing.Callable[[T], T]: """ Decorator for defining a handler function. Args: - name: The name of the handler. + name: The name of the handler. kind: The kind of handler (workflow, shared, exclusive). Default "shared". accept: Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g. `application/*` or `*/*`. Default "application/json". @@ -307,32 +333,34 @@ def my_handler_func(ctx, request): # handler logic pass """ - handler_io = HandlerIO[I,O](accept, content_type, input_serde, output_serde) - def wrapper(fn): + handler_io = HandlerIO[I, O](accept, content_type, input_serde, output_serde) + def wrapper(fn): @wraps(fn) def wrapped(*args, **kwargs): return fn(*args, **kwargs) signature = inspect.signature(fn, eval_str=True) description = inspect.getdoc(fn) - handler = make_handler(service_tag=self.service_tag, - handler_io=handler_io, - name=name, - kind=kind, - wrapped=wrapped, - signature=signature, - description=description, - metadata=metadata, - inactivity_timeout=inactivity_timeout, - abort_timeout=abort_timeout, - journal_retention=journal_retention, - idempotency_retention=idempotency_retention, - workflow_retention=workflow_retention, - enable_lazy_state=enable_lazy_state, - ingress_private=ingress_private, - invocation_retry_policy=invocation_retry_policy) + handler = make_handler( + service_tag=self.service_tag, + handler_io=handler_io, + name=name, + kind=kind, + wrapped=wrapped, + signature=signature, + description=description, + metadata=metadata, + inactivity_timeout=inactivity_timeout, + abort_timeout=abort_timeout, + journal_retention=journal_retention, + idempotency_retention=idempotency_retention, + workflow_retention=workflow_retention, + enable_lazy_state=enable_lazy_state, + ingress_private=ingress_private, + invocation_retry_policy=invocation_retry_policy, + ) self.handlers[handler.name] = handler return wrapped - return wrapper + return typing.cast(typing.Callable[[T], T], wrapper) diff --git a/shell.darwin.nix b/shell.darwin.nix new file mode 100755 index 0000000..0e4b6a4 --- /dev/null +++ b/shell.darwin.nix @@ -0,0 +1,35 @@ +{ pkgs ? import {} }: + +with pkgs; mkShell { + name = "sdk-python"; + buildInputs = [ + python3 + python3Packages.pip + python3Packages.virtualenv + uv + just + rustup + cargo + clang + llvmPackages.bintools + protobuf + cmake + pkg-config + ]; + + RUSTC_VERSION = + builtins.elemAt + (builtins.match + ".*channel *= *\"([^\"]*)\".*" + (pkgs.lib.readFile ./rust-toolchain.toml) + ) + 0; + + LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; + + + shellHook = '' + export UV_PYTHON=$(which python) + ''; + +} diff --git a/shell.nix b/shell.nix deleted file mode 100755 index 54689ba..0000000 --- a/shell.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ pkgs ? import {} }: - -(pkgs.buildFHSEnv { - name = "sdk-python"; - targetPkgs = pkgs: (with pkgs; [ - python3 - python3Packages.pip - python3Packages.virtualenv - just - - # rust - rustup - cargo - clang - llvmPackages.bintools - protobuf - cmake - liburing - pkg-config - ]); - - RUSTC_VERSION = - builtins.elemAt - (builtins.match - ".*channel *= *\"([^\"]*)\".*" - (pkgs.lib.readFile ./rust-toolchain.toml) - ) - 0; - - LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; - - runScript = '' - bash - ''; -}).env diff --git a/test-services/Dockerfile b/test-services/Dockerfile index 31e5159..31602ac 100644 --- a/test-services/Dockerfile +++ b/test-services/Dockerfile @@ -1,6 +1,10 @@ # syntax=docker.io/docker/dockerfile:1.7-labs -FROM ghcr.io/pyo3/maturin AS build-sdk +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim as build-sdk + +ENV UV_PYTHON "3.12" + +RUN apt-get update -y && apt-get install -y build-essential WORKDIR /usr/src/app @@ -13,8 +17,11 @@ COPY requirements.txt . COPY pyproject.toml . COPY LICENSE . COPY README.md . +COPY uv.lock . -RUN maturin build --out dist --interpreter python3.12 + +RUN uv sync --all-extras --all-packages +RUN uv build --all-packages FROM python:3.12-slim AS test-services @@ -22,7 +29,9 @@ WORKDIR /usr/src/app COPY --from=build-sdk /usr/src/app/dist/* /usr/src/app/deps/ -RUN pip install deps/* && pip install hypercorn +RUN pip install deps/*whl +RUN pip install hypercorn + COPY test-services/ . EXPOSE 9080 diff --git a/test-services/requirements.txt b/test-services/requirements.txt deleted file mode 100644 index 949de4a..0000000 --- a/test-services/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -hypercorn -restate_sdk \ No newline at end of file diff --git a/test-services/services/__init__.py b/test-services/services/__init__.py index f5a01f0..2857a84 100644 --- a/test-services/services/__init__.py +++ b/test-services/services/__init__.py @@ -8,13 +8,15 @@ # directory of this repository or package, or at # https://github.com/restatedev/sdk-typescript/blob/main/LICENSE -from typing import Dict, Union from restate import Service, VirtualObject, Workflow +# Import all services so they get registered +# pylint: disable=unused-import +# ruff: noqa: F401 from .counter import counter_object as s1 from .proxy import proxy as s2 from .awakeable_holder import awakeable_holder as s3 -from. block_and_wait_workflow import workflow as s4 +from .block_and_wait_workflow import workflow as s4 from .cancel_test import runner, blocking_service as s5 from .failing import failing as s6 from .kill_test import kill_runner, kill_singleton as s7 @@ -29,14 +31,18 @@ from .interpreter import layer_2 as s14 from .interpreter import helper as s15 + def list_services(bindings): """List all services in this module""" - return {obj.name : obj for _, obj in bindings.items() if isinstance(obj, (Service, VirtualObject, Workflow))} + return {obj.name: obj for _, obj in bindings.items() if isinstance(obj, (Service, VirtualObject, Workflow))} + def services_named(service_names): - return [ _all_services[name] for name in service_names ] + return [_all_services[name] for name in service_names] + def all_services(): return _all_services.values() + _all_services = list_services(locals()) diff --git a/test-services/services/awakeable_holder.py b/test-services/services/awakeable_holder.py index 149dfa3..9e34afc 100644 --- a/test-services/services/awakeable_holder.py +++ b/test-services/services/awakeable_holder.py @@ -18,15 +18,18 @@ awakeable_holder = VirtualObject("AwakeableHolder") + @awakeable_holder.handler() async def hold(ctx: ObjectContext, id: str): ctx.set("id", id) + @awakeable_holder.handler(name="hasAwakeable") async def has_awakeable(ctx: ObjectContext) -> bool: res = await ctx.get("id") return res is not None + @awakeable_holder.handler() async def unlock(ctx: ObjectContext, payload: str): id = await ctx.get("id") diff --git a/test-services/services/block_and_wait_workflow.py b/test-services/services/block_and_wait_workflow.py index 2062ace..00a0853 100644 --- a/test-services/services/block_and_wait_workflow.py +++ b/test-services/services/block_and_wait_workflow.py @@ -18,6 +18,7 @@ workflow = Workflow("BlockAndWaitWorkflow") + @workflow.main() async def run(ctx: WorkflowContext, input: str): ctx.set("my-state", input) @@ -34,6 +35,7 @@ async def run(ctx: WorkflowContext, input: str): async def unblock(ctx: WorkflowSharedContext, output: str): await ctx.promise("durable-promise").resolve(output) + @workflow.handler(name="getState") async def get_state(ctx: WorkflowSharedContext, output: str) -> str | None: return await ctx.get("my-state") diff --git a/test-services/services/cancel_test.py b/test-services/services/cancel_test.py index f8f659b..e3b8364 100644 --- a/test-services/services/cancel_test.py +++ b/test-services/services/cancel_test.py @@ -24,6 +24,7 @@ runner = VirtualObject("CancelTestRunner") + @runner.handler(name="startTest") async def start_test(ctx: ObjectContext, op: BlockingOperation): try: @@ -34,6 +35,7 @@ async def start_test(ctx: ObjectContext, op: BlockingOperation): else: raise t + @runner.handler(name="verifyTest") async def verify_test(ctx: ObjectContext) -> bool: state = await ctx.get("state") @@ -41,9 +43,10 @@ async def verify_test(ctx: ObjectContext) -> bool: return False return state - + blocking_service = VirtualObject("CancelTestBlockingService") + @blocking_service.handler() async def block(ctx: ObjectContext, op: BlockingOperation): name, awakeable = ctx.awakeable() @@ -58,6 +61,7 @@ async def block(ctx: ObjectContext, op: BlockingOperation): name, uncompleteable = ctx.awakeable() await uncompleteable + @blocking_service.handler(name="isUnlocked") async def is_unlocked(ctx: ObjectContext): return None diff --git a/test-services/services/counter.py b/test-services/services/counter.py index 7e5d895..eec9087 100644 --- a/test-services/services/counter.py +++ b/test-services/services/counter.py @@ -57,4 +57,4 @@ async def add_then_fail(ctx: ObjectContext, addend: int): new_value = old_value + addend ctx.set(COUNTER_KEY, new_value) - raise TerminalError(message=ctx.key()) \ No newline at end of file + raise TerminalError(message=ctx.key()) diff --git a/test-services/services/failing.py b/test-services/services/failing.py index 9856d82..3ad7a0a 100644 --- a/test-services/services/failing.py +++ b/test-services/services/failing.py @@ -9,6 +9,7 @@ # https://github.com/restatedev/sdk-typescript/blob/main/LICENSE # """example.py""" + from datetime import timedelta # pylint: disable=C0116 @@ -21,18 +22,22 @@ failing = VirtualObject("Failing") + @failing.handler(name="terminallyFailingCall") async def terminally_failing_call(ctx: ObjectContext, msg: str): raise TerminalError(message=msg) + @failing.handler(name="callTerminallyFailingCall") async def call_terminally_failing_call(ctx: ObjectContext, msg: str) -> str: - await ctx.object_call(terminally_failing_call, key="random-583e1bf2", arg=msg) + await ctx.object_call(terminally_failing_call, key="random-583e1bf2", arg=msg) raise Exception("Should not reach here") + failures = 0 + @failing.handler(name="failingCallWithEventualSuccess") async def failing_call_with_eventual_success(ctx: ObjectContext) -> int: global failures @@ -42,9 +47,9 @@ async def failing_call_with_eventual_success(ctx: ObjectContext) -> int: return 4 raise ValueError(f"Failed at attempt: {failures}") + @failing.handler(name="terminallyFailingSideEffect") async def terminally_failing_side_effect(ctx: ObjectContext, error_message: str): - def side_effect(): raise TerminalError(message=error_message) @@ -54,6 +59,7 @@ def side_effect(): eventual_success_side_effects = 0 + @failing.handler(name="sideEffectSucceedsAfterGivenAttempts") async def side_effect_succeeds_after_given_attempts(ctx: ObjectContext, minimum_attempts: int) -> int: def side_effect(): @@ -63,24 +69,30 @@ def side_effect(): return eventual_success_side_effects raise ValueError(f"Failed at attempt: {eventual_success_side_effects}") - options: RunOptions[int] = RunOptions(max_attempts=minimum_attempts + 1, initial_retry_interval=timedelta(milliseconds=1), retry_interval_factor=1.0) + options: RunOptions[int] = RunOptions( + max_attempts=minimum_attempts + 1, initial_retry_interval=timedelta(milliseconds=1), retry_interval_factor=1.0 + ) return await ctx.run_typed("sideEffect", side_effect, options) + eventual_failure_side_effects = 0 + @failing.handler(name="sideEffectFailsAfterGivenAttempts") async def side_effect_fails_after_given_attempts(ctx: ObjectContext, retry_policy_max_retry_count: int) -> int: - def side_effect(): global eventual_failure_side_effects eventual_failure_side_effects += 1 raise ValueError(f"Failed at attempt: {eventual_failure_side_effects}") try: - options: RunOptions[int] = RunOptions(max_attempts=retry_policy_max_retry_count, initial_retry_interval=timedelta(milliseconds=1), retry_interval_factor=1.0) + options: RunOptions[int] = RunOptions( + max_attempts=retry_policy_max_retry_count, + initial_retry_interval=timedelta(milliseconds=1), + retry_interval_factor=1.0, + ) await ctx.run_typed("sideEffect", side_effect, options) raise ValueError("Side effect did not fail.") - except TerminalError as t: + except TerminalError: global eventual_failure_side_effects return eventual_failure_side_effects - diff --git a/test-services/services/interpreter.py b/test-services/services/interpreter.py index 657b33b..ed17a16 100644 --- a/test-services/services/interpreter.py +++ b/test-services/services/interpreter.py @@ -53,28 +53,32 @@ helper = restate.Service("ServiceInterpreterHelper") + @helper.handler() -async def ping(ctx: Context) -> None: # pylint: disable=unused-argument +async def ping(ctx: Context) -> None: # pylint: disable=unused-argument pass + @helper.handler() -async def echo(ctx: Context, parameters: str) -> str: # pylint: disable=unused-argument +async def echo(ctx: Context, parameters: str) -> str: # pylint: disable=unused-argument return parameters -@helper.handler(name = "echoLater") + +@helper.handler(name="echoLater") async def echo_later(ctx: Context, parameter: dict[str, typing.Any]) -> str: - await ctx.sleep(timedelta(milliseconds=parameter['sleep'])) - return parameter['parameter'] + await ctx.sleep(timedelta(milliseconds=parameter["sleep"])) + return parameter["parameter"] + @helper.handler(name="terminalFailure") async def terminal_failure(ctx: Context) -> str: raise TerminalError("bye") + @helper.handler(name="incrementIndirectly") async def increment_indirectly(ctx: Context, parameter) -> None: - - layer = parameter['layer'] - key = parameter['key'] + layer = parameter["layer"] + key = parameter["key"] program = { "commands": [ @@ -84,23 +88,26 @@ async def increment_indirectly(ctx: Context, parameter) -> None: ], } - program_bytes = json.dumps(program).encode('utf-8') + program_bytes = json.dumps(program).encode("utf-8") ctx.generic_send(f"ObjectInterpreterL{layer}", "interpret", program_bytes, key) + @helper.handler(name="resolveAwakeable") async def resolve_awakeable(ctx: Context, aid: str) -> None: ctx.resolve_awakeable(aid, "ok") + @helper.handler(name="rejectAwakeable") async def reject_awakeable(ctx: Context, aid: str) -> None: ctx.reject_awakeable(aid, "error") + @helper.handler(name="incrementViaAwakeableDance") async def increment_via_awakeable_dance(ctx: Context, input: dict[str, typing.Any]) -> None: - tx_promise_id = input['txPromiseId'] - layer = input['interpreter']['layer'] - key = input['interpreter']['key'] + tx_promise_id = input["txPromiseId"] + layer = input["interpreter"]["layer"] + key = input["interpreter"]["key"] aid, promise = ctx.awakeable() ctx.resolve_awakeable(tx_promise_id, aid) @@ -114,13 +121,12 @@ async def increment_via_awakeable_dance(ctx: Context, input: dict[str, typing.An ], } - program_bytes = json.dumps(program).encode('utf-8') + program_bytes = json.dumps(program).encode("utf-8") ctx.generic_send(f"ObjectInterpreterL{layer}", "interpret", program_bytes, key) class SupportService: - def __init__(self, ctx: ObjectContext) -> None: self.ctx = ctx self.serde = JsonSerde[typing.Any]() @@ -162,7 +168,7 @@ def reject_awakeable(self, aid: str) -> None: self.send("rejectAwakeable", aid) def increment_via_awakeable_dance(self, layer: int, key: str, tx_promise_id: str) -> None: - arg = { "interpreter" : { "layer": layer, "key": key} , "txPromiseId": tx_promise_id } + arg = {"interpreter": {"layer": layer, "key": key}, "txPromiseId": tx_promise_id} self.send("incrementViaAwakeableDance", arg) @@ -172,20 +178,16 @@ class Command(TypedDict): duration: int sleep: int index: int - program: typing.Any # avoid circular type + program: typing.Any # avoid circular type -Program = dict[typing.Literal['commands'], - typing.List[Command]] +Program = dict[typing.Literal["commands"], typing.List[Command]] -async def interpreter(layer: int, - ctx: ObjectContext, - program: Program) -> None: +async def interpreter(layer: int, ctx: ObjectContext, program: Program) -> None: """Interprets a command and executes it.""" service = SupportService(ctx) - coros: dict[int, - typing.Tuple[typing.Any, typing.Awaitable[typing.Any]]] = {} + coros: dict[int, typing.Tuple[typing.Any, typing.Awaitable[typing.Any]]] = {} async def await_promise(index: int) -> None: if index not in coros: @@ -201,8 +203,8 @@ async def await_promise(index: int) -> None: if result != expected: raise TerminalError(f"Expected {expected} but got {result}") - for i, command in enumerate(program['commands']): - command_type = command['kind'] + for i, command in enumerate(program["commands"]): + command_type = command["kind"] if command_type == SET_STATE: ctx.set(f"key-{command['key']}", f"value-{command['key']}") elif command_type == GET_STATE: @@ -214,17 +216,17 @@ async def await_promise(index: int) -> None: c += 1 ctx.set("counter", c) elif command_type == SLEEP: - duration = timedelta(milliseconds=command['duration']) + duration = timedelta(milliseconds=command["duration"]) await ctx.sleep(duration) elif command_type == CALL_SERVICE: expected = f"hello-{i}" coros[i] = (expected, service.echo(expected)) elif command_type == INCREMENT_VIA_DELAYED_CALL: - delay = command['duration'] + delay = command["duration"] await service.increment_indirectly(layer=layer, key=ctx.key(), delay=delay) elif command_type == CALL_SLOW_SERVICE: expected = f"hello-{i}" - coros[i] = (expected, service.echo_later(expected, command['sleep'])) + coros[i] = (expected, service.echo_later(expected, command["sleep"])) elif command_type == SIDE_EFFECT: expected = f"hello-{i}" result = await ctx.run_typed("sideEffect", lambda: expected) @@ -242,6 +244,7 @@ async def await_promise(index: int) -> None: elif command_type == RECOVER_TERMINAL_MAYBE_UN_AWAITED: pass elif command_type == THROWING_SIDE_EFFECT: + async def side_effect(): if bool(random.getrandbits(1)): raise ValueError("Random error") @@ -250,7 +253,7 @@ async def side_effect(): elif command_type == INCREMENT_STATE_COUNTER_INDIRECTLY: await service.increment_indirectly(layer=layer, key=ctx.key()) elif command_type == AWAIT_PROMISE: - index = command['index'] + index = command["index"] await await_promise(index) elif command_type == RESOLVE_AWAKEABLE: name, promise = ctx.awakeable() @@ -268,15 +271,16 @@ async def side_effect(): elif command_type == CALL_NEXT_LAYER_OBJECT: next_layer = f"ObjectInterpreterL{layer + 1}" key = f"{command['key']}" - program = command['program'] + program = command["program"] js_program = json.dumps(program) - raw_js_program = js_program.encode('utf-8') + raw_js_program = js_program.encode("utf-8") promise = ctx.generic_call(next_layer, "interpret", raw_js_program, key) - coros[i] = (b'', promise) + coros[i] = (b"", promise) else: raise ValueError(f"Unknown command type: {command_type}") await await_promise(i) + def make_layer(i): layer = VirtualObject(f"ObjectInterpreterL{i}") diff --git a/test-services/services/kill_test.py b/test-services/services/kill_test.py index abffcf3..10c6cd4 100644 --- a/test-services/services/kill_test.py +++ b/test-services/services/kill_test.py @@ -18,12 +18,15 @@ kill_runner = VirtualObject("KillTestRunner") + @kill_runner.handler(name="startCallTree") async def start_call_tree(ctx: ObjectContext): await ctx.object_call(recursive_call, key=ctx.key(), arg=None) + kill_singleton = VirtualObject("KillTestSingleton") + @kill_singleton.handler(name="recursiveCall") async def recursive_call(ctx: ObjectContext): name, promise = ctx.awakeable() @@ -32,6 +35,7 @@ async def recursive_call(ctx: ObjectContext): await ctx.object_call(recursive_call, key=ctx.key(), arg=None) + @kill_singleton.handler(name="isUnlocked") async def is_unlocked(ctx: ObjectContext): return None diff --git a/test-services/services/list_object.py b/test-services/services/list_object.py index d37b914..777d299 100644 --- a/test-services/services/list_object.py +++ b/test-services/services/list_object.py @@ -16,15 +16,18 @@ list_object = VirtualObject("ListObject") + @list_object.handler() async def append(ctx: ObjectContext, value: str): list = await ctx.get("list") or [] ctx.set("list", list + [value]) + @list_object.handler() async def get(ctx: ObjectContext) -> list[str]: return await ctx.get("list") or [] + @list_object.handler() async def clear(ctx: ObjectContext) -> list[str]: result = await ctx.get("list") or [] diff --git a/test-services/services/map_object.py b/test-services/services/map_object.py index 87cae57..0d335ea 100644 --- a/test-services/services/map_object.py +++ b/test-services/services/map_object.py @@ -22,20 +22,23 @@ class Entry(TypedDict): key: str value: str + @map_object.handler(name="set") async def map_set(ctx: ObjectContext, entry: Entry): ctx.set(entry["key"], entry["value"]) + @map_object.handler(name="get") async def map_get(ctx: ObjectContext, key: str) -> str: return await ctx.get(key) or "" + @map_object.handler(name="clearAll") async def map_clear_all(ctx: ObjectContext) -> list[Entry]: entries = [] for key in await ctx.state_keys(): - value: str = await ctx.get(key) # type: ignore + value: str = await ctx.get(key) # type: ignore entry = Entry(key=key, value=value) entries.append(entry) ctx.clear(key) - return entries \ No newline at end of file + return entries diff --git a/test-services/services/non_determinism.py b/test-services/services/non_determinism.py index 4818e9b..e49eb5a 100644 --- a/test-services/services/non_determinism.py +++ b/test-services/services/non_determinism.py @@ -20,16 +20,20 @@ invoke_counts: Dict[str, int] = {} + def do_left_action(ctx: ObjectContext) -> bool: count_key = ctx.key() invoke_counts[count_key] = invoke_counts.get(count_key, 0) + 1 return invoke_counts[count_key] % 2 == 1 + def increment_counter(ctx: ObjectContext): ctx.object_send(counter.add, key=ctx.key(), arg=1) + non_deterministic = VirtualObject("NonDeterministic") + @non_deterministic.handler(name="setDifferentKey") async def set_different_key(ctx: ObjectContext): if do_left_action(ctx): @@ -39,6 +43,7 @@ async def set_different_key(ctx: ObjectContext): await ctx.sleep(timedelta(milliseconds=100)) increment_counter(ctx) + @non_deterministic.handler(name="backgroundInvokeWithDifferentTargets") async def background_invoke_with_different_targets(ctx: ObjectContext): if do_left_action(ctx): @@ -48,6 +53,7 @@ async def background_invoke_with_different_targets(ctx: ObjectContext): await ctx.sleep(timedelta(milliseconds=100)) increment_counter(ctx) + @non_deterministic.handler(name="callDifferentMethod") async def call_different_method(ctx: ObjectContext): if do_left_action(ctx): @@ -57,6 +63,7 @@ async def call_different_method(ctx: ObjectContext): await ctx.sleep(timedelta(milliseconds=100)) increment_counter(ctx) + @non_deterministic.handler(name="eitherSleepOrCall") async def either_sleep_or_call(ctx: ObjectContext): if do_left_action(ctx): diff --git a/test-services/services/proxy.py b/test-services/services/proxy.py index 9fef286..f0c47a8 100644 --- a/test-services/services/proxy.py +++ b/test-services/services/proxy.py @@ -31,26 +31,28 @@ class ProxyRequest(TypedDict): @proxy.handler() async def call(ctx: Context, req: ProxyRequest) -> Iterable[int]: response = await ctx.generic_call( - req['serviceName'], - req['handlerName'], - bytes(req['message']), - req.get('virtualObjectKey'), - req.get('idempotencyKey')) + req["serviceName"], + req["handlerName"], + bytes(req["message"]), + req.get("virtualObjectKey"), + req.get("idempotencyKey"), + ) return list(response) @proxy.handler(name="oneWayCall") async def one_way_call(ctx: Context, req: ProxyRequest) -> str: send_delay = None - if req.get('delayMillis'): - send_delay = timedelta(milliseconds=req['delayMillis']) + delayMillis = req.get("delayMillis") + if delayMillis is not None: + send_delay = timedelta(milliseconds=delayMillis) handle = ctx.generic_send( - req['serviceName'], - req['handlerName'], - bytes(req['message']), - req.get('virtualObjectKey'), + req["serviceName"], + req["handlerName"], + bytes(req["message"]), + req.get("virtualObjectKey"), send_delay=send_delay, - idempotency_key=req.get('idempotencyKey') + idempotency_key=req.get("idempotencyKey"), ) invocation_id = await handle.invocation_id() return invocation_id @@ -61,31 +63,34 @@ class ManyCallRequest(TypedDict): oneWayCall: bool awaitAtTheEnd: bool + @proxy.handler(name="manyCalls") async def many_calls(ctx: Context, requests: Iterable[ManyCallRequest]): to_await = [] for req in requests: - if req['oneWayCall']: + if req["oneWayCall"]: send_delay = None - if req['proxyRequest'].get('delayMillis'): - send_delay = timedelta(milliseconds=req['proxyRequest']['delayMillis']) + delayMillis = req["proxyRequest"].get("delayMillis") + if delayMillis is not None: + send_delay = timedelta(milliseconds=delayMillis) ctx.generic_send( - req['proxyRequest']['serviceName'], - req['proxyRequest']['handlerName'], - bytes(req['proxyRequest']['message']), - req['proxyRequest'].get('virtualObjectKey'), + req["proxyRequest"]["serviceName"], + req["proxyRequest"]["handlerName"], + bytes(req["proxyRequest"]["message"]), + req["proxyRequest"].get("virtualObjectKey"), send_delay=send_delay, - idempotency_key=req['proxyRequest'].get('idempotencyKey') + idempotency_key=req["proxyRequest"].get("idempotencyKey"), ) else: awaitable = ctx.generic_call( - req['proxyRequest']['serviceName'], - req['proxyRequest']['handlerName'], - bytes(req['proxyRequest']['message']), - req['proxyRequest'].get('virtualObjectKey'), - idempotency_key=req['proxyRequest'].get('idempotencyKey')) - if req['awaitAtTheEnd']: + req["proxyRequest"]["serviceName"], + req["proxyRequest"]["handlerName"], + bytes(req["proxyRequest"]["message"]), + req["proxyRequest"].get("virtualObjectKey"), + idempotency_key=req["proxyRequest"].get("idempotencyKey"), + ) + if req["awaitAtTheEnd"]: to_await.append(awaitable) for awaitable in to_await: diff --git a/test-services/services/test_utils.py b/test-services/services/test_utils.py index 5a1d63d..55ef991 100644 --- a/test-services/services/test_utils.py +++ b/test-services/services/test_utils.py @@ -19,22 +19,33 @@ test_utils = Service("TestUtilsService") + @test_utils.handler() async def echo(context: Context, input: str) -> str: return input + @test_utils.handler(name="uppercaseEcho") async def uppercase_echo(context: Context, input: str) -> str: return input.upper() + @test_utils.handler(name="echoHeaders") async def echo_headers(context: Context) -> Dict[str, str]: return context.request().headers -@test_utils.handler(name="rawEcho", accept="*/*", content_type="application/octet-stream", input_serde=BytesSerde(), output_serde=BytesSerde()) + +@test_utils.handler( + name="rawEcho", + accept="*/*", + content_type="application/octet-stream", + input_serde=BytesSerde(), + output_serde=BytesSerde(), +) async def raw_echo(context: Context, input: bytes) -> bytes: return input + @test_utils.handler(name="sleepConcurrently") async def sleep_concurrently(context: Context, millis_duration: List[int]) -> None: timers = [context.sleep(timedelta(milliseconds=duration)) for duration in millis_duration] @@ -56,6 +67,7 @@ def effect(): return invoked_side_effects + @test_utils.handler(name="cancelInvocation") async def cancel_invocation(context: Context, invocation_id: str) -> None: context.cancel_invocation(invocation_id) diff --git a/test-services/services/virtual_object_command_interpreter.py b/test-services/services/virtual_object_command_interpreter.py index e1914ac..b6015cd 100644 --- a/test-services/services/virtual_object_command_interpreter.py +++ b/test-services/services/virtual_object_command_interpreter.py @@ -21,10 +21,12 @@ virtual_object_command_interpreter = VirtualObject("VirtualObjectCommandInterpreter") + @virtual_object_command_interpreter.handler(name="getResults", kind="shared") async def get_results(ctx: ObjectSharedContext | ObjectContext) -> List[str]: return (await ctx.get("results")) or [] + @virtual_object_command_interpreter.handler(name="hasAwakeable", kind="shared") async def has_awakeable(ctx: ObjectSharedContext, awk_key: str) -> bool: awk_id = await ctx.get("awk-" + awk_key) @@ -32,129 +34,140 @@ async def has_awakeable(ctx: ObjectSharedContext, awk_key: str) -> bool: return True return False + class CreateAwakeable(TypedDict): type: Literal["createAwakeable"] awakeableKey: str + class Sleep(TypedDict): type: Literal["sleep"] timeoutMillis: int + class RunThrowTerminalException(TypedDict): type: Literal["runThrowTerminalException"] reason: str -AwaitableCommand = Union[ - CreateAwakeable, - Sleep, - RunThrowTerminalException -] + +AwaitableCommand = Union[CreateAwakeable, Sleep, RunThrowTerminalException] + class AwaitOne(TypedDict): type: Literal["awaitOne"] command: AwaitableCommand + class AwaitAnySuccessful(TypedDict): type: Literal["awaitAnySuccessful"] commands: List[AwaitableCommand] + class AwaitAny(TypedDict): type: Literal["awaitAny"] commands: List[AwaitableCommand] + class AwaitAwakeableOrTimeout(TypedDict): type: Literal["awaitAwakeableOrTimeout"] awakeableKey: str timeoutMillis: int + class ResolveAwakeable(TypedDict): type: Literal["resolveAwakeable"] awakeableKey: str value: str + class RejectAwakeable(TypedDict): type: Literal["rejectAwakeable"] awakeableKey: str reason: str + class GetEnvVariable(TypedDict): type: Literal["getEnvVariable"] envName: str + Command = Union[ - AwaitOne, - AwaitAny, - AwaitAnySuccessful, - AwaitAwakeableOrTimeout, - ResolveAwakeable, - RejectAwakeable, - GetEnvVariable + AwaitOne, AwaitAny, AwaitAnySuccessful, AwaitAwakeableOrTimeout, ResolveAwakeable, RejectAwakeable, GetEnvVariable ] + class InterpretRequest(TypedDict): commands: Iterable[Command] + @virtual_object_command_interpreter.handler(name="resolveAwakeable", kind="shared") async def resolve_awakeable(ctx: ObjectSharedContext | ObjectContext, req: ResolveAwakeable): - awk_id = await ctx.get("awk-" + req['awakeableKey']) + awk_id = await ctx.get("awk-" + req["awakeableKey"]) if not awk_id: raise TerminalError(message="No awakeable is registered") - ctx.resolve_awakeable(awk_id, req['value']) + ctx.resolve_awakeable(awk_id, req["value"]) + @virtual_object_command_interpreter.handler(name="rejectAwakeable", kind="shared") async def reject_awakeable(ctx: ObjectSharedContext | ObjectContext, req: RejectAwakeable): - awk_id = await ctx.get("awk-" + req['awakeableKey']) + awk_id = await ctx.get("awk-" + req["awakeableKey"]) if not awk_id: raise TerminalError(message="No awakeable is registered") - ctx.reject_awakeable(awk_id, req['reason']) + ctx.reject_awakeable(awk_id, req["reason"]) + def to_durable_future(ctx: ObjectContext, cmd: AwaitableCommand) -> RestateDurableFuture[Any]: - if cmd['type'] == "createAwakeable": + if cmd["type"] == "createAwakeable": awk_id, awakeable = ctx.awakeable() - ctx.set("awk-" + cmd['awakeableKey'], awk_id) + ctx.set("awk-" + cmd["awakeableKey"], awk_id) return awakeable - elif cmd['type'] == "sleep": - return ctx.sleep(timedelta(milliseconds=cmd['timeoutMillis'])) - elif cmd['type'] == "runThrowTerminalException": + elif cmd["type"] == "sleep": + return ctx.sleep(timedelta(milliseconds=cmd["timeoutMillis"])) + elif cmd["type"] == "runThrowTerminalException": + def side_effect(reason: str): raise TerminalError(message=reason) - res = ctx.run_typed("run should fail command", side_effect, reason=cmd['reason']) + + res = ctx.run_typed("run should fail command", side_effect, reason=cmd["reason"]) return res + @virtual_object_command_interpreter.handler(name="interpretCommands") async def interpret_commands(ctx: ObjectContext, req: InterpretRequest): result = "" - for cmd in req['commands']: - if cmd['type'] == "awaitAwakeableOrTimeout": + for cmd in req["commands"]: + if cmd["type"] == "awaitAwakeableOrTimeout": awk_id, awakeable = ctx.awakeable() - ctx.set("awk-" + cmd['awakeableKey'], awk_id) - match await select(awakeable=awakeable, timeout=ctx.sleep(timedelta(milliseconds=cmd['timeoutMillis']))): - case ['awakeable', awk_res]: + ctx.set("awk-" + cmd["awakeableKey"], awk_id) + match await select(awakeable=awakeable, timeout=ctx.sleep(timedelta(milliseconds=cmd["timeoutMillis"]))): + case ["awakeable", awk_res]: result = awk_res - case ['timeout', _]: + case ["timeout", _]: raise TerminalError(message="await-timeout", status_code=500) - elif cmd['type'] == "resolveAwakeable": + elif cmd["type"] == "resolveAwakeable": await resolve_awakeable(ctx, cmd) result = "" - elif cmd['type'] == "rejectAwakeable": + elif cmd["type"] == "rejectAwakeable": await reject_awakeable(ctx, cmd) result = "" - elif cmd['type'] == "getEnvVariable": - env_name = cmd['envName'] + elif cmd["type"] == "getEnvVariable": + env_name = cmd["envName"] + def side_effect(env_name: str): return os.environ.get(env_name, "") + result = await ctx.run_typed("get_env", side_effect, env_name=env_name) - elif cmd['type'] == "awaitOne": - awaitable = to_durable_future(ctx, cmd['command']) + elif cmd["type"] == "awaitOne": + awaitable = to_durable_future(ctx, cmd["command"]) # We need this dance because the Python SDK doesn't support .map on futures if isinstance(awaitable, RestateDurableSleepFuture): await awaitable result = "sleep" else: result = await awaitable - elif cmd['type'] == "awaitAny": - futures = [to_durable_future(ctx, c) for c in cmd['commands']] + elif cmd["type"] == "awaitAny": + futures = [to_durable_future(ctx, c) for c in cmd["commands"]] done, _ = await wait_completed(*futures) done_fut = done[0] # We need this dance because the Python SDK doesn't support .map on futures @@ -163,8 +176,8 @@ def side_effect(env_name: str): result = "sleep" else: result = await done_fut - elif cmd['type'] == "awaitAnySuccessful": - futures = [to_durable_future(ctx, c) for c in cmd['commands']] + elif cmd["type"] == "awaitAnySuccessful": + futures = [to_durable_future(ctx, c) for c in cmd["commands"]] async for done_fut in as_completed(*futures): try: # We need this dance because the Python SDK doesn't support .map on futures @@ -182,4 +195,3 @@ def side_effect(env_name: str): ctx.set("results", last_results) return result - diff --git a/test-services/testservices.py b/test-services/testservices.py index abf5597..0da224e 100644 --- a/test-services/testservices.py +++ b/test-services/testservices.py @@ -16,13 +16,14 @@ import restate import services + def test_services(): - names = os.environ.get('SERVICES') - return services.services_named(names.split(',')) if names else services.all_services() + names = os.environ.get("SERVICES") + return services.services_named(names.split(",")) if names else services.all_services() + -identity_keys = None -e2e_signing_key_env = os.environ.get('E2E_REQUEST_SIGNING_ENV') -if os.environ.get('E2E_REQUEST_SIGNING_ENV'): - identity_keys = [os.environ.get('E2E_REQUEST_SIGNING_ENV')] +e2e_signing_key_env = os.environ.get("E2E_REQUEST_SIGNING_ENV") +if e2e_signing_key_env is not None: + e2e_signing_key_env = [e2e_signing_key_env] -app = restate.app(services=test_services(), identity_keys=identity_keys) +app = restate.app(services=test_services(), identity_keys=e2e_signing_key_env) diff --git a/tests/serde.py b/tests/serde.py index 71c8fef..506874e 100644 --- a/tests/serde.py +++ b/tests/serde.py @@ -1,5 +1,6 @@ from restate.serde import BytesSerde + def test_bytes_serde(): s = BytesSerde() - assert bytes(range(20)) == s.serialize(bytes(range(20))) \ No newline at end of file + assert bytes(range(20)) == s.serialize(bytes(range(20))) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9cf3398 --- /dev/null +++ b/uv.lock @@ -0,0 +1,804 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dacite" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09", size = 22420, upload-time = "2025-02-05T09:27:29.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hypercorn" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "h11" }, + { name = "h2" }, + { name = "priority" }, + { name = "taskgroup", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/3a/df6c27642e0dcb7aff688ca4be982f0fb5d89f2afd3096dc75347c16140f/hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165", size = 44409, upload-time = "2024-05-28T20:55:53.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/3b/dfa13a8d96aa24e40ea74a975a9906cfdc2ab2f4e3b498862a57052f04eb/hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547", size = 61742, upload-time = "2024-05-28T20:55:48.829Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "priority" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792, upload-time = "2021-06-27T10:15:05.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946, upload-time = "2021-06-27T10:15:03.856Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.407" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "restate-sdk" +source = { editable = "." } + +[package.optional-dependencies] +harness = [ + { name = "httpx" }, + { name = "hypercorn" }, + { name = "testcontainers" }, +] +lint = [ + { name = "mypy" }, + { name = "pyright" }, + { name = "ruff" }, +] +serde = [ + { name = "dacite" }, + { name = "pydantic" }, +] +test = [ + { name = "hypercorn" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "dacite", marker = "extra == 'serde'" }, + { name = "httpx", marker = "extra == 'harness'" }, + { name = "hypercorn", marker = "extra == 'harness'" }, + { name = "hypercorn", marker = "extra == 'test'" }, + { name = "mypy", marker = "extra == 'lint'", specifier = ">=1.11.2" }, + { name = "pydantic", marker = "extra == 'serde'" }, + { name = "pyright", marker = "extra == 'lint'", specifier = ">=1.1.390" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "ruff", marker = "extra == 'lint'", specifier = ">=0.6.9" }, + { name = "testcontainers", marker = "extra == 'harness'" }, +] +provides-extras = ["test", "lint", "harness", "serde"] + +[[package]] +name = "ruff" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/55/cccfca45157a2031dcbb5a462a67f7cf27f8b37d4b3b1cd7438f0f5c1df6/ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3", size = 5587844, upload-time = "2025-11-06T22:07:45.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/b9/67240254166ae1eaa38dec32265e9153ac53645a6c6670ed36ad00722af8/ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518", size = 12606781, upload-time = "2025-11-06T22:07:01.841Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/09b3ab245d8652eafe5256ab59718641429f68681ee713ff06c5c549f156/ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4", size = 12946765, upload-time = "2025-11-06T22:07:05.858Z" }, + { url = "https://files.pythonhosted.org/packages/14/bb/1564b000219144bf5eed2359edc94c3590dd49d510751dad26202c18a17d/ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33", size = 11928120, upload-time = "2025-11-06T22:07:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/a3/92/d5f1770e9988cc0742fefaa351e840d9aef04ec24ae1be36f333f96d5704/ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2", size = 12370877, upload-time = "2025-11-06T22:07:10.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/29/e9282efa55f1973d109faf839a63235575519c8ad278cc87a182a366810e/ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5", size = 12408538, upload-time = "2025-11-06T22:07:13.085Z" }, + { url = "https://files.pythonhosted.org/packages/8e/01/930ed6ecfce130144b32d77d8d69f5c610e6d23e6857927150adf5d7379a/ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e", size = 13141942, upload-time = "2025-11-06T22:07:15.386Z" }, + { url = "https://files.pythonhosted.org/packages/6a/46/a9c89b42b231a9f487233f17a89cbef9d5acd538d9488687a02ad288fa6b/ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8", size = 14544306, upload-time = "2025-11-06T22:07:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/9c6cf86491f2a6d52758b830b89b78c2ae61e8ca66b86bf5a20af73d20e6/ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649", size = 14210427, upload-time = "2025-11-06T22:07:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/71/f4/0666fe7769a54f63e66404e8ff698de1dcde733e12e2fd1c9c6efb689cb5/ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850", size = 13658488, upload-time = "2025-11-06T22:07:22.32Z" }, + { url = "https://files.pythonhosted.org/packages/ee/79/6ad4dda2cfd55e41ac9ed6d73ef9ab9475b1eef69f3a85957210c74ba12c/ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5", size = 13354908, upload-time = "2025-11-06T22:07:24.347Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/f0b6990f740bb15c1588601d19d21bcc1bd5de4330a07222041678a8e04f/ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132", size = 13587803, upload-time = "2025-11-06T22:07:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/c9/da/eaaada586f80068728338e0ef7f29ab3e4a08a692f92eb901a4f06bbff24/ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67", size = 12279654, upload-time = "2025-11-06T22:07:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b1d0e82cf9bf8aed10a6d45be47b3f402730aa2c438164424783ac88c0ed/ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469", size = 12357520, upload-time = "2025-11-06T22:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/53e2b42cc82804617e5c7950b7079d79996c27e99c4652131c6a1100657f/ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde", size = 12719431, upload-time = "2025-11-06T22:07:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/a2/94/80e3d74ed9a72d64e94a7b7706b1c1ebaa315ef2076fd33581f6a1cd2f95/ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349", size = 13464394, upload-time = "2025-11-06T22:07:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/1a/a49f071f04c42345c793d22f6cf5e0920095e286119ee53a64a3a3004825/ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff", size = 12493429, upload-time = "2025-11-06T22:07:38.43Z" }, + { url = "https://files.pythonhosted.org/packages/bc/22/e58c43e641145a2b670328fb98bc384e20679b5774258b1e540207580266/ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c", size = 13635380, upload-time = "2025-11-06T22:07:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/4168a751ddbbf43e86544b4de8b5c3b7be8d7167a2a5cb977d274e04f0a1/ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb", size = 12663065, upload-time = "2025-11-06T22:07:42.603Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "taskgroup" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237, upload-time = "2025-01-03T09:24:11.41Z" }, +] + +[[package]] +name = "testcontainers" +version = "4.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/ce/4fd72abe8372cc8c737c62da9dadcdfb6921b57ad8932f7a0feb605e5bf5/testcontainers-4.13.1.tar.gz", hash = "sha256:4a6c5b2faa3e8afb91dff18b389a14b485f3e430157727b58e65d30c8dcde3f3", size = 77955, upload-time = "2025-09-24T22:47:47.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/30/f0660686920e09680b8afb0d2738580223dbef087a9bd92f3f14163c2fa6/testcontainers-4.13.1-py3-none-any.whl", hash = "sha256:10e6013a215eba673a0bcc153c8809d6f1c53c245e0a236e3877807652af4952", size = 123995, upload-time = "2025-09-24T22:47:45.44Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +]